From a63aa8f90c785571595c855bdcb175570a770714 Mon Sep 17 00:00:00 2001 From: Arjun Date: Wed, 4 Oct 2023 11:00:44 +0530 Subject: [PATCH 01/14] feat: Mileage/ Per Diem expenses creation to depend on category (#2461) * initial changes for dashboard * done * fix failing tests * pr comment fi * added categories service spy to tests * minor * remove unwanted changes * minor * remove fdescribe * test: Mileage/ Per Diem expenses to depend on category (#2468) * coverage * test description update * fix tests * fix * more tests --- .../mock-data/action-sheet-options.data.ts | 42 +++++++++++++++++ .../mock-data/allowed-expense-types.data.ts | 4 ++ src/app/core/mock-data/org-category.data.ts | 28 +++++++++++ .../core/services/categories.service.spec.ts | 17 +++++++ src/app/core/services/categories.service.ts | 13 +++++ src/app/fyle/dashboard/dashboard.page.html | 13 +++-- src/app/fyle/dashboard/dashboard.page.scss | 7 +++ src/app/fyle/dashboard/dashboard.page.spec.ts | 45 +++++++++++++++--- src/app/fyle/dashboard/dashboard.page.ts | 27 ++++++++--- .../fyle/my-expenses/my-expenses.page.html | 15 ++++-- .../fyle/my-expenses/my-expenses.page.scss | 7 +++ .../fyle/my-expenses/my-expenses.page.spec.ts | 47 ++++++++++++++++--- src/app/fyle/my-expenses/my-expenses.page.ts | 36 ++++++++++---- 13 files changed, 265 insertions(+), 36 deletions(-) create mode 100644 src/app/core/mock-data/allowed-expense-types.data.ts diff --git a/src/app/core/mock-data/action-sheet-options.data.ts b/src/app/core/mock-data/action-sheet-options.data.ts index f8b0d47f42..59c080b37f 100644 --- a/src/app/core/mock-data/action-sheet-options.data.ts +++ b/src/app/core/mock-data/action-sheet-options.data.ts @@ -46,3 +46,45 @@ export const expectedActionSheetButtonRes = [ handler: undefined, }, ]; + +export const expectedActionSheetButtonsWithMileage = [ + { + text: 'Capture Receipt', + icon: 'assets/svg/fy-camera.svg', + cssClass: 'capture-receipt', + handler: undefined, + }, + { + text: 'Add Manually', + icon: 'assets/svg/fy-expense.svg', + cssClass: 'capture-receipt', + handler: undefined, + }, + { + text: 'Add Mileage', + icon: 'assets/svg/fy-mileage.svg', + cssClass: 'capture-receipt', + handler: undefined, + }, +]; + +export const expectedActionSheetButtonsWithPerDiem = [ + { + text: 'Capture Receipt', + icon: 'assets/svg/fy-camera.svg', + cssClass: 'capture-receipt', + handler: undefined, + }, + { + text: 'Add Manually', + icon: 'assets/svg/fy-expense.svg', + cssClass: 'capture-receipt', + handler: undefined, + }, + { + text: 'Add Per Diem', + icon: 'assets/svg/fy-calendar.svg', + cssClass: 'capture-receipt', + handler: undefined, + }, +]; diff --git a/src/app/core/mock-data/allowed-expense-types.data.ts b/src/app/core/mock-data/allowed-expense-types.data.ts new file mode 100644 index 0000000000..dd41359380 --- /dev/null +++ b/src/app/core/mock-data/allowed-expense-types.data.ts @@ -0,0 +1,4 @@ +export const allowedExpenseTypes: Record = { + mileage: true, + perDiem: true, +}; diff --git a/src/app/core/mock-data/org-category.data.ts b/src/app/core/mock-data/org-category.data.ts index 9e69cfa385..1c42ded879 100644 --- a/src/app/core/mock-data/org-category.data.ts +++ b/src/app/core/mock-data/org-category.data.ts @@ -1,3 +1,4 @@ +import { PlatformCategory } from '../models/platform/platform-category.model'; import { OrgCategory } from '../models/v1/org-category.model'; export const orgCategoryData: OrgCategory = { @@ -13,6 +14,33 @@ export const orgCategoryData: OrgCategory = { updated_at: new Date('2022-05-05T17:45:42.092507+00:00'), }; +export const mileagePerDiemPlatformCategoryData: PlatformCategory[] = [ + { + code: null, + created_at: new Date('2018-01-31T23:50:27.235056+00:00'), + display_name: 'Mileage', + is_enabled: true, + system_category: 'Mileage', + id: 16566, + name: 'Mileage', + org_id: 'orNVthTo2Zyo', + sub_category: 'Mileage', + updated_at: new Date('2022-05-05T17:45:42.092507+00:00'), + }, + { + code: null, + created_at: new Date('2018-01-31T23:50:27.235056+00:00'), + display_name: 'Per Diem', + is_enabled: true, + system_category: 'Per Diem', + id: 16566, + name: 'Per Diem', + org_id: 'orNVthTo2Zyo', + sub_category: 'Per Diem', + updated_at: new Date('2022-05-05T17:45:42.092507+00:00'), + }, +]; + export const transformedOrgCategories: OrgCategory[] = [ { code: '93', diff --git a/src/app/core/services/categories.service.spec.ts b/src/app/core/services/categories.service.spec.ts index 080c14899a..cf2350a90d 100644 --- a/src/app/core/services/categories.service.spec.ts +++ b/src/app/core/services/categories.service.spec.ts @@ -96,6 +96,23 @@ describe('CategoriesService', () => { }); }); + it('getMileageOrPerDiemCategories(): should get platform categories with Mileage and Per Diem as system category', (done) => { + spenderPlatformV1ApiService.get.and.returnValue(of(platformApiCategoryRes)); + + const apiParam = { + params: { + is_enabled: 'eq.true', + system_category: 'in.(Mileage, Per Diem)', + }, + }; + + categoriesService.getMileageOrPerDiemCategories().subscribe((res) => { + expect(res).toEqual(platformApiCategoryRes.data); + expect(spenderPlatformV1ApiService.get).toHaveBeenCalledOnceWith('/categories', apiParam); + done(); + }); + }); + it('getCategories(): should get categories from the api', (done) => { spenderPlatformV1ApiService.get.and.returnValue(of(platformApiAllCategories)); spyOn(categoriesService, 'transformFrom').and.returnValue(transformedOrgCategories); diff --git a/src/app/core/services/categories.service.ts b/src/app/core/services/categories.service.ts index da91966656..0df6eecec6 100644 --- a/src/app/core/services/categories.service.ts +++ b/src/app/core/services/categories.service.ts @@ -78,6 +78,19 @@ export class CategoriesService { ); } + @Cacheable() + getMileageOrPerDiemCategories(): Observable { + const data = { + params: { + is_enabled: 'eq.true', + system_category: 'in.(Mileage, Per Diem)', + }, + }; + return this.spenderPlatformV1ApiService + .get>('/categories', data) + .pipe(map((res) => res.data)); + } + transformFrom(platformCategory: PlatformCategory[]): OrgCategory[] { const oldCategory = platformCategory.map((category) => ({ code: category.code, diff --git a/src/app/fyle/dashboard/dashboard.page.html b/src/app/fyle/dashboard/dashboard.page.html index 1fae44bfd6..fba456ebbc 100644 --- a/src/app/fyle/dashboard/dashboard.page.html +++ b/src/app/fyle/dashboard/dashboard.page.html @@ -30,9 +30,16 @@ Home -
- -
+ +
+ +
+
+ +
+ +
+
diff --git a/src/app/fyle/dashboard/dashboard.page.scss b/src/app/fyle/dashboard/dashboard.page.scss index 9a1a4c3134..f3ea25047a 100644 --- a/src/app/fyle/dashboard/dashboard.page.scss +++ b/src/app/fyle/dashboard/dashboard.page.scss @@ -60,5 +60,12 @@ &--action-shortcut { padding-right: 12px; padding-top: 2px; + + &--skeleton-icon { + margin-right: 12px; + width: 24px; + height: 24px; + border-radius: 5px; + } } } diff --git a/src/app/fyle/dashboard/dashboard.page.spec.ts b/src/app/fyle/dashboard/dashboard.page.spec.ts index 9c0cefaa32..3bdaab90e1 100644 --- a/src/app/fyle/dashboard/dashboard.page.spec.ts +++ b/src/app/fyle/dashboard/dashboard.page.spec.ts @@ -17,9 +17,16 @@ import { Subject, Subscription, of } from 'rxjs'; import { orgSettingsRes } from 'src/app/core/mock-data/org-settings.data'; import { orgUserSettingsData } from 'src/app/core/mock-data/org-user-settings.data'; import { BackButtonActionPriority } from 'src/app/core/models/back-button-action-priority.enum'; -import { cloneDeep } from 'lodash'; -import { expectedActionSheetButtonRes } from 'src/app/core/mock-data/action-sheet-options.data'; +import { clone, cloneDeep } from 'lodash'; +import { + expectedActionSheetButtonRes, + expectedActionSheetButtonsWithMileage, + expectedActionSheetButtonsWithPerDiem, +} from 'src/app/core/mock-data/action-sheet-options.data'; import { creditTxnFilterPill } from 'src/app/core/mock-data/filter-pills.data'; +import { allowedExpenseTypes } from 'src/app/core/mock-data/allowed-expense-types.data'; +import { CategoriesService } from 'src/app/core/services/categories.service'; +import { mileagePerDiemPlatformCategoryData } from 'src/app/core/mock-data/org-category.data'; describe('DashboardPage', () => { let component: DashboardPage; @@ -34,6 +41,7 @@ describe('DashboardPage', () => { let smartlookService: jasmine.SpyObj; let orgSettingsService: jasmine.SpyObj; let orgUserSettingsService: jasmine.SpyObj; + let categoriesService: jasmine.SpyObj; let backButtonService: jasmine.SpyObj; let platform: Platform; let navController: jasmine.SpyObj; @@ -53,6 +61,7 @@ describe('DashboardPage', () => { let smartlookServiceSpy = jasmine.createSpyObj('SmartlookService', ['init']); let orgSettingsServiceSpy = jasmine.createSpyObj('OrgSettingsService', ['get']); let orgUserSettingsServiceSpy = jasmine.createSpyObj('OrgUserSettingsService', ['get']); + const categoriesServiceSpy = jasmine.createSpyObj('CategoriesService', ['getMileageOrPerDiemCategories']); let backButtonServiceSpy = jasmine.createSpyObj('BackButtonService', ['showAppCloseAlert']); let navControllerSpy = jasmine.createSpyObj('NavController', ['back']); @@ -69,6 +78,7 @@ describe('DashboardPage', () => { { provide: SmartlookService, useValue: smartlookServiceSpy }, { provide: OrgSettingsService, useValue: orgSettingsServiceSpy }, { provide: OrgUserSettingsService, useValue: orgUserSettingsServiceSpy }, + { provide: CategoriesService, useValue: categoriesServiceSpy }, { provide: BackButtonService, useValue: backButtonServiceSpy }, { provide: NavController, useValue: navControllerSpy }, Platform, @@ -98,6 +108,7 @@ describe('DashboardPage', () => { smartlookService = TestBed.inject(SmartlookService) as jasmine.SpyObj; orgSettingsService = TestBed.inject(OrgSettingsService) as jasmine.SpyObj; orgUserSettingsService = TestBed.inject(OrgUserSettingsService) as jasmine.SpyObj; + categoriesService = TestBed.inject(CategoriesService) as jasmine.SpyObj; backButtonService = TestBed.inject(BackButtonService) as jasmine.SpyObj; platform = TestBed.inject(Platform); navController = TestBed.inject(NavController) as jasmine.SpyObj; @@ -171,6 +182,7 @@ describe('DashboardPage', () => { spyOn(component, 'registerBackButtonAction'); orgSettingsService.get.and.returnValue(of(orgSettingsRes)); orgUserSettingsService.get.and.returnValue(of(orgUserSettingsData)); + categoriesService.getMileageOrPerDiemCategories.and.returnValue(of(mileagePerDiemPlatformCategoryData)); currencyService.getHomeCurrency.and.returnValue(of('USD')); spyOn(component, 'setupActionSheet'); const statsComponentSpy = jasmine.createSpyObj('StatsComponent', ['init']); @@ -228,7 +240,7 @@ describe('DashboardPage', () => { it('should call setupActionSheet once with orgSettings data', () => { component.ionViewWillEnter(); - expect(component.setupActionSheet).toHaveBeenCalledOnceWith(orgSettingsRes); + expect(component.setupActionSheet).toHaveBeenCalledOnceWith(orgSettingsRes, allowedExpenseTypes); }); it('should call init method of statsComponent and tasksComponent', () => { @@ -416,13 +428,32 @@ describe('DashboardPage', () => { }); }); - it('setupActionSheet(): should setup actionSheetButtons', () => { + describe('setupActionSheet()', () => { const mockOrgSettings = cloneDeep(orgSettingsRes); - spyOn(component, 'actionSheetButtonsHandler'); mockOrgSettings.per_diem.enabled = true; mockOrgSettings.mileage.enabled = true; - component.setupActionSheet(mockOrgSettings); - expect(component.actionSheetButtons).toEqual(expectedActionSheetButtonRes); + + it('should setup actionSheetButtons', () => { + spyOn(component, 'actionSheetButtonsHandler'); + component.setupActionSheet(orgSettingsRes, allowedExpenseTypes); + expect(component.actionSheetButtons).toEqual(expectedActionSheetButtonRes); + }); + + it('should update actionSheetButtons without mileage', () => { + spyOn(component, 'actionSheetButtonsHandler'); + const mockAllowedExpenseTypes = clone(allowedExpenseTypes); + mockAllowedExpenseTypes.mileage = false; + component.setupActionSheet(orgSettingsRes, mockAllowedExpenseTypes); + expect(component.actionSheetButtons).toEqual(expectedActionSheetButtonsWithPerDiem); + }); + + it('should update actionSheetButtons without Per Diem', () => { + spyOn(component, 'actionSheetButtonsHandler'); + const mockAllowedExpenseTypes = clone(allowedExpenseTypes); + mockAllowedExpenseTypes.perDiem = false; + component.setupActionSheet(orgSettingsRes, mockAllowedExpenseTypes); + expect(component.actionSheetButtons).toEqual(expectedActionSheetButtonsWithMileage); + }); }); it('openAddExpenseActionSheet(): should open actionSheetController and track event', fakeAsync(() => { diff --git a/src/app/fyle/dashboard/dashboard.page.ts b/src/app/fyle/dashboard/dashboard.page.ts index 6cc8f2e094..2d8540f3b0 100644 --- a/src/app/fyle/dashboard/dashboard.page.ts +++ b/src/app/fyle/dashboard/dashboard.page.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, ViewChild } from '@angular/core'; -import { concat, Observable, of, Subject, Subscription } from 'rxjs'; +import { concat, forkJoin, Observable, of, Subject, Subscription } from 'rxjs'; import { shareReplay, switchMap, takeUntil } from 'rxjs/operators'; import { ActionSheetButton, ActionSheetController, NavController, Platform } from '@ionic/angular'; import { NetworkService } from '../../core/services/network.service'; @@ -19,6 +19,8 @@ import { BackButtonService } from 'src/app/core/services/back-button.service'; import { OrgSettings } from 'src/app/core/models/org-settings.model'; import { FilterPill } from 'src/app/shared/components/fy-filter-pills/filter-pill.interface'; import { CardStatsComponent } from './card-stats/card-stats.component'; +import { PlatformCategory } from 'src/app/core/models/platform/platform-category.model'; +import { CategoriesService } from 'src/app/core/services/categories.service'; enum DashboardState { home, @@ -41,6 +43,8 @@ export class DashboardPage { orgSettings$: Observable; + specialCategories$: Observable; + homeCurrency$: Observable; isConnected$: Observable; @@ -66,6 +70,7 @@ export class DashboardPage { private smartlookService: SmartlookService, private orgUserSettingsService: OrgUserSettingsService, private orgSettingsService: OrgSettingsService, + private categoriesService: CategoriesService, private platform: Platform, private backButtonService: BackButtonService, private navController: NavController @@ -116,10 +121,18 @@ export class DashboardPage { this.orgUserSettings$ = this.orgUserSettingsService.get().pipe(shareReplay(1)); this.orgSettings$ = this.orgSettingsService.get().pipe(shareReplay(1)); + this.specialCategories$ = this.categoriesService.getMileageOrPerDiemCategories().pipe(shareReplay(1)); this.homeCurrency$ = this.currencyService.getHomeCurrency().pipe(shareReplay(1)); - this.orgSettings$.subscribe((orgSettings) => { - this.setupActionSheet(orgSettings); + forkJoin({ + orgSettings: this.orgSettings$, + specialCategories: this.specialCategories$, + }).subscribe(({ orgSettings, specialCategories }) => { + const allowedExpenseTypes = { + mileage: specialCategories.some((category) => category.system_category === 'Mileage'), + perDiem: specialCategories.some((category) => category.system_category === 'Per Diem'), + }; + this.setupActionSheet(orgSettings, allowedExpenseTypes); }); this.statsComponent.init(); @@ -229,10 +242,10 @@ export class DashboardPage { }; } - setupActionSheet(orgSettings: OrgSettings): void { + setupActionSheet(orgSettings: OrgSettings, allowedExpenseTypes: Record): void { const that = this; - const mileageEnabled = orgSettings.mileage.enabled; - const isPerDiemEnabled = orgSettings.per_diem.enabled; + const mileageEnabled = orgSettings.mileage.enabled && allowedExpenseTypes.mileage; + const isPerDiemEnabled = orgSettings.per_diem.enabled && allowedExpenseTypes.perDiem; that.actionSheetButtons = [ { text: 'Capture Receipt', @@ -249,7 +262,7 @@ export class DashboardPage { ]; if (mileageEnabled) { - this.actionSheetButtons.push({ + that.actionSheetButtons.push({ text: 'Add Mileage', icon: 'assets/svg/fy-mileage.svg', cssClass: 'capture-receipt', diff --git a/src/app/fyle/my-expenses/my-expenses.page.html b/src/app/fyle/my-expenses/my-expenses.page.html index c9ba3c0ad1..04e2d89f44 100644 --- a/src/app/fyle/my-expenses/my-expenses.page.html +++ b/src/app/fyle/my-expenses/my-expenses.page.html @@ -19,9 +19,18 @@ - - - + + + + + + + +
+ +
+
+
diff --git a/src/app/fyle/my-expenses/my-expenses.page.scss b/src/app/fyle/my-expenses/my-expenses.page.scss index 9219913334..48399ec77e 100644 --- a/src/app/fyle/my-expenses/my-expenses.page.scss +++ b/src/app/fyle/my-expenses/my-expenses.page.scss @@ -10,6 +10,13 @@ --padding-end: 8px !important; } + &--header-btn--skeleton-loader { + margin: 12px 8px; + border-radius: 5px; + width: 24px; + height: 24px; + } + &--filter-pills { position: fixed; width: 100%; diff --git a/src/app/fyle/my-expenses/my-expenses.page.spec.ts b/src/app/fyle/my-expenses/my-expenses.page.spec.ts index fe8dabbc06..d345546c67 100644 --- a/src/app/fyle/my-expenses/my-expenses.page.spec.ts +++ b/src/app/fyle/my-expenses/my-expenses.page.spec.ts @@ -66,8 +66,12 @@ import { ExpenseFilters } from './expense-filters.model'; import { txnData2, txnList } from 'src/app/core/mock-data/transaction.data'; import { unformattedTxnData } from 'src/app/core/mock-data/unformatted-transaction.data'; import { expenseFiltersData1, expenseFiltersData2 } from 'src/app/core/mock-data/expense-filters.data'; -import { expectedActionSheetButtonRes } from 'src/app/core/mock-data/action-sheet-options.data'; -import { cloneDeep } from 'lodash'; +import { + expectedActionSheetButtonRes, + expectedActionSheetButtonsWithMileage, + expectedActionSheetButtonsWithPerDiem, +} from 'src/app/core/mock-data/action-sheet-options.data'; +import { clone, cloneDeep } from 'lodash'; import { apiAuthRes } from 'src/app/core/mock-data/auth-reponse.data'; import { LoaderService } from 'src/app/core/services/loader.service'; import { PopupService } from 'src/app/core/services/popup.service'; @@ -104,6 +108,9 @@ import { FyDeleteDialogComponent } from 'src/app/shared/components/fy-delete-dia import { getElementRef } from 'src/app/core/dom-helpers'; import { transactionDatum1, transactionDatum3 } from 'src/app/core/mock-data/stats-response.data'; import { uniqueCardsParam } from 'src/app/core/mock-data/unique-cards.data'; +import { allowedExpenseTypes } from 'src/app/core/mock-data/allowed-expense-types.data'; +import { CategoriesService } from 'src/app/core/services/categories.service'; +import { mileagePerDiemPlatformCategoryData } from 'src/app/core/mock-data/org-category.data'; describe('MyExpensesPage', () => { let component: MyExpensesPage; @@ -128,6 +135,7 @@ describe('MyExpensesPage', () => { let storageService: jasmine.SpyObj; let corporateCreditCardService: jasmine.SpyObj; let orgUserSettingsService: jasmine.SpyObj; + let categoriesService: jasmine.SpyObj; let platformHandlerService: jasmine.SpyObj; let trackingService: jasmine.SpyObj; let modalController: jasmine.SpyObj; @@ -176,6 +184,7 @@ describe('MyExpensesPage', () => { 'deleteBulk', ]); const orgSettingsServiceSpy = jasmine.createSpyObj('OrgSettingsService', ['get']); + const categoriesServiceSpy = jasmine.createSpyObj('CategoriesService', ['getMileageOrPerDiemCategories']); const navControllerSpy = jasmine.createSpyObj('NavController', ['back']); const networkServiceSpy = jasmine.createSpyObj('NetworkService', ['isOnline', 'connectivityWatcher']); const activatedRouteSpy = { @@ -326,6 +335,10 @@ describe('MyExpensesPage', () => { provide: SnackbarPropertiesService, useValue: snackbarPropertiesSpy, }, + { + provide: CategoriesService, + useValue: categoriesServiceSpy, + }, ReportState, MaskNumber, ], @@ -344,6 +357,7 @@ describe('MyExpensesPage', () => { reportService = TestBed.inject(ReportService) as jasmine.SpyObj; tasksService = TestBed.inject(TasksService) as jasmine.SpyObj; orgSettingsService = TestBed.inject(OrgSettingsService) as jasmine.SpyObj; + categoriesService = TestBed.inject(CategoriesService) as jasmine.SpyObj; apiV2Service = TestBed.inject(ApiV2Service) as jasmine.SpyObj; transactionService = TestBed.inject(TransactionService) as jasmine.SpyObj; networkService = TestBed.inject(NetworkService) as jasmine.SpyObj; @@ -390,6 +404,7 @@ describe('MyExpensesPage', () => { platformHandlerService.registerBackButtonAction.and.returnValue(backButtonSubscription); orgUserSettingsService.get.and.returnValue(of(orgUserSettingsData)); orgSettingsService.get.and.returnValue(of(orgSettingsRes)); + categoriesService.getMileageOrPerDiemCategories.and.returnValue(of(mileagePerDiemPlatformCategoryData)); corporateCreditCardService.getAssignedCards.and.returnValue(of(expectedAssignedCCCStats)); spyOn(component, 'getCardDetail').and.returnValue(expectedUniqueCardStats); spyOn(component, 'syncOutboxExpenses'); @@ -549,7 +564,7 @@ describe('MyExpensesPage', () => { component.ionViewWillEnter(); tick(500); - expect(component.setupActionSheet).toHaveBeenCalledOnceWith(orgSettingsRes); + expect(component.setupActionSheet).toHaveBeenCalledOnceWith(orgSettingsRes, allowedExpenseTypes); })); it('should update cardNumbers by calling getCardDetail', fakeAsync(() => { @@ -1100,10 +1115,28 @@ describe('MyExpensesPage', () => { }); }); }); - it('setupActionSheet(): should update actionSheetButtons', () => { - spyOn(component, 'actionSheetButtonsHandler'); - component.setupActionSheet(orgSettingsRes); - expect(component.actionSheetButtons).toEqual(expectedActionSheetButtonRes); + describe('setupActionSheet()', () => { + it('should update actionSheetButtons', () => { + spyOn(component, 'actionSheetButtonsHandler'); + component.setupActionSheet(orgSettingsRes, allowedExpenseTypes); + expect(component.actionSheetButtons).toEqual(expectedActionSheetButtonRes); + }); + + it('should update actionSheetButtons without mileage', () => { + spyOn(component, 'actionSheetButtonsHandler'); + const mockAllowedExpenseTypes = clone(allowedExpenseTypes); + mockAllowedExpenseTypes.mileage = false; + component.setupActionSheet(orgSettingsRes, mockAllowedExpenseTypes); + expect(component.actionSheetButtons).toEqual(expectedActionSheetButtonsWithPerDiem); + }); + + it('should update actionSheetButtons without Per Diem', () => { + spyOn(component, 'actionSheetButtonsHandler'); + const mockAllowedExpenseTypes = clone(allowedExpenseTypes); + mockAllowedExpenseTypes.perDiem = false; + component.setupActionSheet(orgSettingsRes, mockAllowedExpenseTypes); + expect(component.actionSheetButtons).toEqual(expectedActionSheetButtonsWithMileage); + }); }); describe('actionSheetButtonsHandler():', () => { diff --git a/src/app/fyle/my-expenses/my-expenses.page.ts b/src/app/fyle/my-expenses/my-expenses.page.ts index 40ba68f199..8eb53f3d9d 100644 --- a/src/app/fyle/my-expenses/my-expenses.page.ts +++ b/src/app/fyle/my-expenses/my-expenses.page.ts @@ -76,6 +76,8 @@ import { UniqueCardStats } from 'src/app/core/models/unique-cards-stats.model'; import { FilterQueryParams } from 'src/app/core/models/filter-query-params.model'; import { SelectedFilters } from 'src/app/shared/components/fy-filters/selected-filters.interface'; import { UniqueCards } from 'src/app/core/models/unique-cards.model'; +import { CategoriesService } from 'src/app/core/services/categories.service'; +import { PlatformCategory } from 'src/app/core/models/platform/platform-category.model'; @Component({ selector: 'app-my-expenses', @@ -115,6 +117,10 @@ export class MyExpensesPage implements OnInit { isPerDiemEnabled$: Observable; + orgSettings$: Observable; + + specialCategories$: Observable; + pendingTransactions: Partial[] = []; selectionMode = false; @@ -208,6 +214,7 @@ export class MyExpensesPage implements OnInit { private currencyService: CurrencyService, private orgUserSettingsService: OrgUserSettingsService, private platformHandlerService: PlatformHandlerService, + private categoriesService: CategoriesService, private navController: NavController ) {} @@ -337,10 +344,10 @@ export class MyExpensesPage implements OnInit { }; } - setupActionSheet(orgSettings: OrgSettings): void { + setupActionSheet(orgSettings: OrgSettings, allowedExpenseTypes: Record): void { const that = this; - const mileageEnabled = orgSettings.mileage.enabled; - const isPerDiemEnabled = orgSettings.per_diem.enabled; + const mileageEnabled = orgSettings.mileage.enabled && allowedExpenseTypes.mileage; + const isPerDiemEnabled = orgSettings.per_diem.enabled && allowedExpenseTypes.perDiem; that.actionSheetButtons = [ { text: 'Capture Receipt', @@ -357,7 +364,7 @@ export class MyExpensesPage implements OnInit { ]; if (mileageEnabled) { - this.actionSheetButtons.push({ + that.actionSheetButtons.push({ text: 'Add Mileage', icon: 'assets/svg/fy-mileage.svg', cssClass: 'capture-receipt', @@ -428,14 +435,25 @@ export class MyExpensesPage implements OnInit { map((orgUserSettings) => orgUserSettings?.bulk_fyle_settings?.enabled) ); - const getOrgSettingsService$ = this.orgSettingsService.get().pipe(shareReplay(1)); + this.orgSettings$ = this.orgSettingsService.get().pipe(shareReplay(1)); + this.specialCategories$ = this.categoriesService.getMileageOrPerDiemCategories().pipe(shareReplay(1)); - this.isMileageEnabled$ = getOrgSettingsService$.pipe(map((orgSettings) => orgSettings?.mileage?.enabled)); - this.isPerDiemEnabled$ = getOrgSettingsService$.pipe(map((orgSettings) => orgSettings?.per_diem?.enabled)); + this.isMileageEnabled$ = this.orgSettings$.pipe(map((orgSettings) => orgSettings?.mileage?.enabled)); + this.isPerDiemEnabled$ = this.orgSettings$.pipe(map((orgSettings) => orgSettings?.per_diem?.enabled)); - getOrgSettingsService$.subscribe((orgSettings) => { + this.orgSettings$.subscribe((orgSettings) => { this.isNewReportsFlowEnabled = orgSettings?.simplified_report_closure_settings?.enabled || false; - this.setupActionSheet(orgSettings); + }); + + forkJoin({ + orgSettings: this.orgSettings$, + specialCategories: this.specialCategories$, + }).subscribe(({ orgSettings, specialCategories }) => { + const allowedExpenseTypes = { + mileage: specialCategories.some((category) => category.system_category === 'Mileage'), + perDiem: specialCategories.some((category) => category.system_category === 'Per Diem'), + }; + this.setupActionSheet(orgSettings, allowedExpenseTypes); }); forkJoin({ From d50f3f723fff0efb7edcb45e4ba98014340156a1 Mon Sep 17 00:00:00 2001 From: Shishira M S <31820553+shishirams222@users.noreply.github.com> Date: Wed, 4 Oct 2023 11:04:36 +0530 Subject: [PATCH 02/14] FYLE-category-autofill-fix: updated condition for category autofill (#2459) --- src/app/core/mock-data/unflattened-expense.data.ts | 2 +- .../fyle/add-edit-expense/add-edit-expense-3.spec.ts | 2 +- .../fyle/add-edit-expense/add-edit-expense.page.ts | 11 ++++++++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/app/core/mock-data/unflattened-expense.data.ts b/src/app/core/mock-data/unflattened-expense.data.ts index e2f6ac018c..718ab60a66 100644 --- a/src/app/core/mock-data/unflattened-expense.data.ts +++ b/src/app/core/mock-data/unflattened-expense.data.ts @@ -554,7 +554,7 @@ export const draftUnflattendedTxn3 = { tx: { ...unflattenedExpData.tx, id: 'txCYDX0peUw5', - source: 'MOBILE', + source: 'WEBAPP_BULK', state: 'DRAFT', org_category_id: null, fyle_category: 'UNSPECIFIED', diff --git a/src/app/fyle/add-edit-expense/add-edit-expense-3.spec.ts b/src/app/fyle/add-edit-expense/add-edit-expense-3.spec.ts index 8067eb631f..8bd55c7d7e 100644 --- a/src/app/fyle/add-edit-expense/add-edit-expense-3.spec.ts +++ b/src/app/fyle/add-edit-expense/add-edit-expense-3.spec.ts @@ -368,7 +368,7 @@ export function TestCases3(getTestBed) { } }); - it('should get auto fill category for DRAFT expense', () => { + it('should get auto fill category for DRAFT expense added via webapp bulk upload or bulk instafyle', () => { const result = component.getAutofillCategory({ isAutofillsEnabled: true, recentValue: recentlyUsedRes, 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 2e9075f3cc..bab4ce4627 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 @@ -1927,13 +1927,18 @@ export class AddEditExpensePage implements OnInit { const isCategoryEmpty = !etxn.tx.org_category_id || etxn.tx.fyle_category?.toLowerCase() === 'unspecified'; /* - * Autofill should be applied iff: + * Autofill should be applied if: * - Autofilled is allowed and enabled for the user * - The user has some recently used categories present + * - isTxnEligibleForCategoryAutofill: * - The transaction category is empty or 'unspecified' - * - The user is on creating a new expense or editing a DRAFT expense + * - The user is on creating a new expense or editing a DRAFT expense that was created from bulk upload or bulk instafyle */ - if (doRecentOrgCategoryIdsExist && isCategoryEmpty && (!etxn.tx.id || etxn.tx.state === 'DRAFT')) { + const isNewExpense = !etxn.tx.id; + const canAutofillCategoryDuringEdit = + etxn.tx.state === 'DRAFT' && ['WEBAPP_BULK', 'MOBILE_DASHCAM_BULK'].includes(etxn.tx.source); + const isTxnEligibleForCategoryAutofill = isCategoryEmpty && (isNewExpense || canAutofillCategoryDuringEdit); + if (doRecentOrgCategoryIdsExist && isTxnEligibleForCategoryAutofill) { const autoFillCategory = recentCategories?.length && recentCategories[0]; if (autoFillCategory) { From 09e8d7cb85d8b36e5bf315244008dacc6f5c5f19 Mon Sep 17 00:00:00 2001 From: Omkar Joshi <65808188+OmkarJ13@users.noreply.github.com> Date: Wed, 4 Oct 2023 17:07:56 +0530 Subject: [PATCH 03/14] feat: Remove unified card enrollment flow LD flag (#2473) --- .../card-stats/card-stats.component.spec.ts | 17 ++++------------ .../card-stats/card-stats.component.ts | 20 +++---------------- src/app/fyle/my-profile/my-profile.page.html | 4 +--- .../fyle/my-profile/my-profile.page.spec.ts | 9 --------- src/app/fyle/my-profile/my-profile.page.ts | 13 +----------- 5 files changed, 9 insertions(+), 54 deletions(-) diff --git a/src/app/fyle/dashboard/card-stats/card-stats.component.spec.ts b/src/app/fyle/dashboard/card-stats/card-stats.component.spec.ts index c3cf024360..0b2e489545 100644 --- a/src/app/fyle/dashboard/card-stats/card-stats.component.spec.ts +++ b/src/app/fyle/dashboard/card-stats/card-stats.component.spec.ts @@ -23,7 +23,6 @@ import { By } from '@angular/platform-browser'; import { cardDetailsRes } from 'src/app/core/mock-data/platform-corporate-card-detail-data'; import { AddCorporateCardComponent } from '../../manage-corporate-cards/add-corporate-card/add-corporate-card.component'; import { CardAddedComponent } from '../../manage-corporate-cards/card-added/card-added.component'; -import { LaunchDarklyService } from 'src/app/core/services/launch-darkly.service'; @Component({ selector: 'app-spent-cards', @@ -62,7 +61,6 @@ describe('CardStatsComponent', () => { let orgUserSettingsService: jasmine.SpyObj; let corporateCreditCardExpenseService: jasmine.SpyObj; let popoverController: jasmine.SpyObj; - let launchDarklyService: jasmine.SpyObj; beforeEach(waitForAsync(() => { const currencyServiceSpy = jasmine.createSpyObj('CurrencyService', ['getHomeCurrency']); @@ -76,7 +74,6 @@ describe('CardStatsComponent', () => { 'clearCache', ]); const popoverControllerSpy = jasmine.createSpyObj('PopoverController', ['create']); - const launchDarklyServiceSpy = jasmine.createSpyObj('LaunchDarklyService', ['getVariation']); TestBed.configureTestingModule({ declarations: [CardStatsComponent, MockSpentCardsComponent, MockAddCardComponent], @@ -110,10 +107,6 @@ describe('CardStatsComponent', () => { provide: PopoverController, useValue: popoverControllerSpy, }, - { - provide: LaunchDarklyService, - useValue: launchDarklyServiceSpy, - }, ], }).compileComponents(); @@ -126,10 +119,9 @@ describe('CardStatsComponent', () => { networkService = TestBed.inject(NetworkService) as jasmine.SpyObj; orgUserSettingsService = TestBed.inject(OrgUserSettingsService) as jasmine.SpyObj; corporateCreditCardExpenseService = TestBed.inject( - CorporateCreditCardExpenseService, + CorporateCreditCardExpenseService ) as jasmine.SpyObj; popoverController = TestBed.inject(PopoverController) as jasmine.SpyObj; - launchDarklyService = TestBed.inject(LaunchDarklyService) as jasmine.SpyObj; // Default return values currencyService.getHomeCurrency.and.returnValue(of('USD')); @@ -140,7 +132,6 @@ describe('CardStatsComponent', () => { corporateCreditCardExpenseService.getPlatformCorporateCardDetails.and.returnValue(cardDetails); networkService.isOnline.and.returnValue(of(true)); corporateCreditCardExpenseService.clearCache.and.returnValue(of(null)); - launchDarklyService.getVariation.and.returnValue(of(true)); spyOn(component.loadCardDetails$, 'next').and.callThrough(); @@ -192,7 +183,7 @@ describe('CardStatsComponent', () => { expect(corporateCreditCardExpenseService.getCorporateCards).toHaveBeenCalledTimes(1); expect(corporateCreditCardExpenseService.getPlatformCorporateCardDetails).toHaveBeenCalledOnceWith( cards, - cardStats.cardDetails, + cardStats.cardDetails ); }); @@ -245,7 +236,7 @@ describe('CardStatsComponent', () => { popoverController.create.and.returnValues( Promise.resolve(addCardPopoverSpy), - Promise.resolve(cardAddedPopoverSpy), + Promise.resolve(cardAddedPopoverSpy) ); corporateCreditCardExpenseService.getCorporateCards.and.returnValue(of([])); @@ -310,7 +301,7 @@ describe('CardStatsComponent', () => { popoverController.create.and.returnValues( Promise.resolve(addCardPopoverSpy), - Promise.resolve(cardAddedPopoverSpy), + Promise.resolve(cardAddedPopoverSpy) ); component.ngOnInit(); diff --git a/src/app/fyle/dashboard/card-stats/card-stats.component.ts b/src/app/fyle/dashboard/card-stats/card-stats.component.ts index 869edac23d..f3c038a776 100644 --- a/src/app/fyle/dashboard/card-stats/card-stats.component.ts +++ b/src/app/fyle/dashboard/card-stats/card-stats.component.ts @@ -12,7 +12,6 @@ import { AddCorporateCardComponent } from '../../manage-corporate-cards/add-corp import { OverlayResponse } from 'src/app/core/models/overlay-response.modal'; import { CardAddedComponent } from '../../manage-corporate-cards/card-added/card-added.component'; import { NetworkService } from 'src/app/core/services/network.service'; -import { LaunchDarklyService } from 'src/app/core/services/launch-darkly.service'; @Component({ selector: 'app-card-stats', @@ -47,8 +46,7 @@ export class CardStatsComponent implements OnInit { private networkService: NetworkService, private orgUserSettingsService: OrgUserSettingsService, private corporateCreditCardExpenseService: CorporateCreditCardExpenseService, - private popoverController: PopoverController, - private launchDarklyService: LaunchDarklyService + private popoverController: PopoverController ) {} ngOnInit(): void { @@ -98,20 +96,8 @@ export class CardStatsComponent implements OnInit { ) ); - const isUnifiedCardEnrollmentFlowEnabled$ = this.launchDarklyService.getVariation( - 'unified_card_enrollment_flow_enabled', - false - ); - - this.canAddCorporateCards$ = forkJoin([ - isUnifiedCardEnrollmentFlowEnabled$, - this.isVisaRTFEnabled$, - this.isMastercardRTFEnabled$, - ]).pipe( - map( - ([isUnifiedCardEnrollmentFlowEnabled, isVisaRTFEnabled, isMastercardRTFEnabled]) => - isUnifiedCardEnrollmentFlowEnabled && (isVisaRTFEnabled || isMastercardRTFEnabled) - ) + this.canAddCorporateCards$ = forkJoin([this.isVisaRTFEnabled$, this.isMastercardRTFEnabled$]).pipe( + map(([isVisaRTFEnabled, isMastercardRTFEnabled]) => isVisaRTFEnabled || isMastercardRTFEnabled) ); this.cardDetails$ = this.loadCardDetails$.pipe( diff --git a/src/app/fyle/my-profile/my-profile.page.html b/src/app/fyle/my-profile/my-profile.page.html index 343e04d9df..90edf33ba1 100644 --- a/src/app/fyle/my-profile/my-profile.page.html +++ b/src/app/fyle/my-profile/my-profile.page.html @@ -46,9 +46,7 @@ > - +
Corporate Cards
Manage Corporate Cards
diff --git a/src/app/fyle/my-profile/my-profile.page.spec.ts b/src/app/fyle/my-profile/my-profile.page.spec.ts index a4f8ceede1..c5a5f982b9 100644 --- a/src/app/fyle/my-profile/my-profile.page.spec.ts +++ b/src/app/fyle/my-profile/my-profile.page.spec.ts @@ -31,7 +31,6 @@ import { MyProfilePage } from './my-profile.page'; import { UpdateMobileNumberComponent } from './update-mobile-number/update-mobile-number.component'; import { VerifyNumberPopoverComponent } from './verify-number-popover/verify-number-popover.component'; import { orgData1 } from 'src/app/core/mock-data/org.data'; -import { LaunchDarklyService } from 'src/app/core/services/launch-darkly.service'; describe('MyProfilePage', () => { let component: MyProfilePage; @@ -52,7 +51,6 @@ describe('MyProfilePage', () => { let matSnackBar: jasmine.SpyObj; let snackbarProperties: jasmine.SpyObj; let activatedRoute: jasmine.SpyObj; - let launchDarklyService: jasmine.SpyObj; beforeEach(waitForAsync(() => { const authServiceSpy = jasmine.createSpyObj('AuthService', ['getEou', 'logout', 'refreshEou']); @@ -76,7 +74,6 @@ describe('MyProfilePage', () => { const popoverControllerSpy = jasmine.createSpyObj('PopoverController', ['create']); const matSnackBarSpy = jasmine.createSpyObj('MatSnackBar', ['openFromComponent']); const snackbarPropertiesSpy = jasmine.createSpyObj('SnackbarPropertiesService', ['setSnackbarProperties']); - const launchDarklyServiceSpy = jasmine.createSpyObj('LaunchDarklyService', ['getVariation']); TestBed.configureTestingModule({ declarations: [MyProfilePage], @@ -156,10 +153,6 @@ describe('MyProfilePage', () => { provide: SnackbarPropertiesService, useValue: snackbarPropertiesSpy, }, - { - provide: LaunchDarklyService, - useValue: launchDarklyServiceSpy, - }, ], }).compileComponents(); @@ -183,9 +176,7 @@ describe('MyProfilePage', () => { matSnackBar = TestBed.inject(MatSnackBar) as jasmine.SpyObj; snackbarProperties = TestBed.inject(SnackbarPropertiesService) as jasmine.SpyObj; activatedRoute = TestBed.inject(ActivatedRoute) as jasmine.SpyObj; - launchDarklyService = TestBed.inject(LaunchDarklyService) as jasmine.SpyObj; - launchDarklyService.getVariation.and.returnValue(of(true)); component.loadEou$ = new BehaviorSubject(null); component.eou$ = of(apiEouRes); diff --git a/src/app/fyle/my-profile/my-profile.page.ts b/src/app/fyle/my-profile/my-profile.page.ts index 41064721d5..863b17bc83 100644 --- a/src/app/fyle/my-profile/my-profile.page.ts +++ b/src/app/fyle/my-profile/my-profile.page.ts @@ -32,7 +32,6 @@ import { OverlayResponse } from 'src/app/core/models/overlay-response.modal'; import { EventData } from 'src/app/core/models/event-data.model'; import { PreferenceSetting } from 'src/app/core/models/preference-setting.model'; import { CopyCardDetails } from 'src/app/core/models/copy-card-details.model'; -import { LaunchDarklyService } from 'src/app/core/services/launch-darkly.service'; @Component({ selector: 'app-my-profile', @@ -74,10 +73,6 @@ export class MyProfilePage { isMastercardRTFEnabled: boolean; - isYodleeEnabled: boolean; - - isUnifiedCardEnrollmentFlowEnabled: boolean; - constructor( private authService: AuthService, private orgUserSettingsService: OrgUserSettingsService, @@ -94,8 +89,7 @@ export class MyProfilePage { private popoverController: PopoverController, private matSnackBar: MatSnackBar, private snackbarProperties: SnackbarPropertiesService, - private activatedRoute: ActivatedRoute, - private launchDarklyService: LaunchDarklyService + private activatedRoute: ActivatedRoute ) {} setupNetworkWatcher(): void { @@ -187,10 +181,6 @@ export class MyProfilePage { forkJoin({ orgUserSettings: orgUserSettings$, orgSettings: orgSettings$, - isUnifiedCardEnrollmentFlowEnabled: this.launchDarklyService.getVariation( - 'unified_card_enrollment_flow_enabled', - false - ), }) ), finalize(() => from(this.loaderService.hideLoader())) @@ -198,7 +188,6 @@ export class MyProfilePage { .subscribe(async (res) => { this.orgUserSettings = res.orgUserSettings; this.orgSettings = res.orgSettings; - this.isUnifiedCardEnrollmentFlowEnabled = res.isUnifiedCardEnrollmentFlowEnabled; this.setCCCFlags(); this.setPreferenceSettings(); From de1a71f8b2c6fd191bc3726a8a4d9469c5d1ecb9 Mon Sep 17 00:00:00 2001 From: Suyash Patil <127177049+suyashpatil78@users.noreply.github.com> Date: Thu, 5 Oct 2023 10:31:14 +0530 Subject: [PATCH 04/14] test: 40% coverage of add-edit-adv-request pagee (#2467) * fix: lint issues in add edit adv request page * test: 12% coverage of add-edit-advance-request page * minor * test: 26% coverage of add edit adv request page * test: 40% coverage of add-edit-adv-request pagee * test: 58% coverage add-edit-adv-request page (#2476) * test: 45% coverage of add edit adv request * 58% coverage of page * minor * test: 70% coverage of add-edit-adv-req page (#2479) * test: 67% coverage of page * 70% coverage of page * minor * minor * test: 87% coverage of add-edit-adv-request page (#2481) * test: 87% coverage of add-edit-adv-request page * test: 98.45% coverage of add-edit-adv-request page (#2482) * test: 97% coverage * 98% coverage * pr fix * pr comments --- ...dd-edit-advance-request-form-value.data.ts | 6 + ...dvance-request-custom-field-values.data.ts | 40 ++ .../advance-requests-custom-fields.data.ts | 45 ++ .../core/mock-data/advance-requests.data.ts | 18 + src/app/core/mock-data/file-object.data.ts | 49 ++ src/app/core/mock-data/file.data.ts | 18 + .../core/mock-data/modal-controller.data.ts | 57 ++ .../unflattened-advance-request.data.ts | 74 +++ .../advance-request-delete-params.model.ts | 14 + .../add-edit-advance-request-1.page.spec.ts | 163 +++++- .../add-edit-advance-request-2.page.spec.ts | 489 ++++++++++++++++++ ...dd-edit-advance-request.page.setup.spec.ts | 15 +- .../add-edit-advance-request.page.ts | 57 +- 13 files changed, 1005 insertions(+), 40 deletions(-) create mode 100644 src/app/core/mock-data/advance-request-custom-field-values.data.ts create mode 100644 src/app/core/mock-data/unflattened-advance-request.data.ts create mode 100644 src/app/core/models/advance-request-delete-params.model.ts create mode 100644 src/app/fyle/add-edit-advance-request/add-edit-advance-request-2.page.spec.ts diff --git a/src/app/core/mock-data/add-edit-advance-request-form-value.data.ts b/src/app/core/mock-data/add-edit-advance-request-form-value.data.ts index ca16115f04..73c0f1b26a 100644 --- a/src/app/core/mock-data/add-edit-advance-request-form-value.data.ts +++ b/src/app/core/mock-data/add-edit-advance-request-form-value.data.ts @@ -1,4 +1,5 @@ import { AddEditAdvanceRequestFormValue } from '../models/add-edit-advance-request-form-value.model'; +import { recentlyUsedProjectRes } from './recently-used.data'; export const addEditAdvanceRequestFormValueData: AddEditAdvanceRequestFormValue = { currencyObj: { @@ -20,3 +21,8 @@ export const addEditAdvanceRequestFormValueData2: AddEditAdvanceRequestFormValue project: null, customFieldValues: [], }; + +export const addEditAdvanceRequestFormValueData3: AddEditAdvanceRequestFormValue = { + ...addEditAdvanceRequestFormValueData, + project: recentlyUsedProjectRes[0], +}; diff --git a/src/app/core/mock-data/advance-request-custom-field-values.data.ts b/src/app/core/mock-data/advance-request-custom-field-values.data.ts new file mode 100644 index 0000000000..5f117f659d --- /dev/null +++ b/src/app/core/mock-data/advance-request-custom-field-values.data.ts @@ -0,0 +1,40 @@ +import { AdvanceRequestCustomFieldValues } from '../models/advance-request-custom-field-values.model'; + +export const advanceRequestCustomFieldValuesData: AdvanceRequestCustomFieldValues[] = [ + { + id: 1302, + name: 'Phase', + value: 'Phase 1', + type: 'SELECT', + }, + { + id: 1305, + name: 'BILLABLE', + value: true, + type: 'BOOLEAN', + }, + { + id: 1304, + name: 'Arrival Date', + value: '2 Jan 2023', + type: 'DATE', + }, +]; + +export const advanceRequestCustomFieldValuesData2: AdvanceRequestCustomFieldValues[] = [ + { + id: 1302, + name: 'Phase', + value: 'Phase 1', + }, + { + id: 1304, + name: 'Arrival Date', + value: '2023-1-2', + }, + { + id: 1305, + name: 'BILLABLE', + value: true, + }, +]; diff --git a/src/app/core/mock-data/advance-requests-custom-fields.data.ts b/src/app/core/mock-data/advance-requests-custom-fields.data.ts index e052438c57..df6750db53 100644 --- a/src/app/core/mock-data/advance-requests-custom-fields.data.ts +++ b/src/app/core/mock-data/advance-requests-custom-fields.data.ts @@ -43,3 +43,48 @@ export const advanceRequestCustomFieldData: AdvanceRequestsCustomFields[] = [ placeholder: '123', }, ]; + +export const advanceRequestCustomFieldData2: AdvanceRequestsCustomFields[] = [ + { + id: 150, + org_id: 'orNVthTo2Zyo', + created_at: new Date('2022-10-30T23:07:03.385Z'), + updated_at: new Date('2022-10-30T23:07:03.385Z'), + type: 'BOOLEAN', + name: 'checking', + options: ['option1', 'option2'], + mandatory: false, + active: true, + added_by: 'ouX8dwsbLCLv', + last_updated_by: 'ouX8dwsbLCLv', + placeholder: null, + }, + { + id: 142, + org_id: 'orNVthTo2Zyo', + created_at: new Date('2022-11-04T02:14:37.292Z'), + updated_at: new Date('2022-11-04T02:14:37.292Z'), + type: 'BOOLEAN', + name: 'Okay?', + options: null, + mandatory: false, + active: true, + added_by: 'ouX8dwsbLCLv', + last_updated_by: 'ouX8dwsbLCLv', + placeholder: null, + }, + { + id: 144, + org_id: 'orNVthTo2Zyo', + created_at: new Date('2022-11-04T02:14:37.292Z'), + updated_at: new Date('2022-11-04T02:14:37.292Z'), + type: 'BOOLEAN', + name: 'Okay?', + options: null, + mandatory: false, + active: true, + added_by: 'ouX8dwsbLCLv', + last_updated_by: 'ouX8dwsbLCLv', + placeholder: null, + }, +]; diff --git a/src/app/core/mock-data/advance-requests.data.ts b/src/app/core/mock-data/advance-requests.data.ts index 7ffb1b9aa3..657c569950 100644 --- a/src/app/core/mock-data/advance-requests.data.ts +++ b/src/app/core/mock-data/advance-requests.data.ts @@ -379,3 +379,21 @@ export const checkPolicyAdvReqParam: AdvanceRequests = { is_sent_back: null, is_pulled_back: true, }; + +export const advanceRequests2: Partial = { + ...advanceRequests, + currency: 'USD', + amount: 130, + purpose: 'Test purpose', + project_id: 168826, + notes: 'Test notes', + source: 'MOBILE', + custom_field_values: null, +}; + +export const advanceRequests3: Partial = { + org_user_id: 'ouX8dwsbLCLv', + currency: 'GNF', + source: 'MOBILE', + created_at: new Date(), +}; diff --git a/src/app/core/mock-data/file-object.data.ts b/src/app/core/mock-data/file-object.data.ts index c99fc9c83f..8655f0fd8f 100644 --- a/src/app/core/mock-data/file-object.data.ts +++ b/src/app/core/mock-data/file-object.data.ts @@ -310,3 +310,52 @@ export const expectedFileData1 = [ thumbnail: 'thumbnail', }, ]; + +export const advanceRequestFileUrlData: FileObject[] = [ + { + ...fileObjectAdv1, + }, + { + ...fileObject7[0], + type: 'jpeg', + id: null, + }, +]; + +export const expectedFileData2: FileObject[] = [ + { + type: 'pdf', + url: '2023-02-08/orNVthTo2Zyo/receipts/fi6PQ6z4w6ET.000.pdf', + thumbnail: '2023-02-08/orNVthTo2Zyo/receipts/fi6PQ6z4w6ET.000.pdf', + }, +]; + +export const advanceRequestFileUrlData2: FileObject[] = [ + { + ...fileObjectAdv1, + id: null, + }, + { + ...fileObject7[0], + type: 'image', + id: null, + }, +]; + +export const fileObject9: FileObject[] = [ + { + id: 'fiV1gXpyCcbU', + org_user_id: 'ouX8dwsbLCLv', + created_at: new Date('2023-03-06T07:51:05.614Z'), + name: '000.jpeg', + s3url: '2023-03-06/orNVthTo2Zyo/receipts/fiV1gXpyCcbU.000.jpeg', + transaction_id: 'tx1vdITUXIzf', + invoice_id: null, + advance_request_id: null, + purpose: 'ORIGINAL', + password: null, + receipt_coordinates: null, + email_meta_data: null, + fyle_sub_url: '/api/files/fiV1gXpyCcbU/download', + }, +]; diff --git a/src/app/core/mock-data/file.data.ts b/src/app/core/mock-data/file.data.ts index 05fced21b7..0aad6c231e 100644 --- a/src/app/core/mock-data/file.data.ts +++ b/src/app/core/mock-data/file.data.ts @@ -35,3 +35,21 @@ export const fileData2: File[] = [ fyle_sub_url: '/api/files/fiK7c69UDJNb/download', }, ]; + +export const fileData3: File[] = [ + { + id: 'fiV1gXpyCcbU', + org_user_id: 'ouX8dwsbLCLv', + created_at: new Date('2023-03-06T07:51:05.614Z'), + name: '000.jpeg', + s3url: '2023-03-06/orNVthTo2Zyo/receipts/fiV1gXpyCcbU.000.jpeg', + transaction_id: 'tx1vdITUXIzf', + invoice_id: null, + advance_request_id: null, + purpose: 'ORIGINAL', + password: null, + receipt_coordinates: null, + email_meta_data: null, + fyle_sub_url: '/api/files/fiV1gXpyCcbU/download', + }, +]; diff --git a/src/app/core/mock-data/modal-controller.data.ts b/src/app/core/mock-data/modal-controller.data.ts index dde0e6dddd..a9b57276d1 100644 --- a/src/app/core/mock-data/modal-controller.data.ts +++ b/src/app/core/mock-data/modal-controller.data.ts @@ -15,6 +15,10 @@ import { reportOptionsData } from './report-options.data'; import { expectedErpt } from './report-unflattened.data'; import { FyInputPopoverComponent } from 'src/app/shared/components/fy-input-popover/fy-input-popover.component'; import { PolicyViolationDialogComponent } from 'src/app/fyle/add-edit-advance-request/policy-violation-dialog/policy-violation-dialog.component'; +import { CaptureReceiptComponent } from 'src/app/shared/components/capture-receipt/capture-receipt.component'; +import { FyViewAttachmentComponent } from 'src/app/shared/components/fy-view-attachment/fy-view-attachment.component'; +import { advanceRequestFileUrlData2 } from './file-object.data'; +import { ViewCommentComponent } from 'src/app/shared/components/comments-history/view-comment/view-comment.component'; export const modalControllerParams = { component: FyFiltersComponent, @@ -387,3 +391,56 @@ export const advanceRequestPolicyViolationParams = { breakpoints: [0, 1], handle: false, }; + +export const popoverControllerParams4 = { + component: PopupAlertComponent, + componentProps: { + title: 'Review', + message: + 'This action will save a draft advance request and will not be submitted to your approvers directly. You need to explicitly submit a draft advance request.', + primaryCta: { + text: 'Finish', + action: 'continue', + }, + secondaryCta: { + text: 'Cancel', + action: 'cancel', + }, + }, + cssClass: 'pop-up-in-center', +}; + +export const modalControllerParams3 = { + component: CaptureReceiptComponent, + componentProps: { + isModal: true, + allowGalleryUploads: false, + allowBulkFyle: false, + }, + cssClass: 'hide-modal', +}; + +export const modalControllerParams4 = { + component: FyViewAttachmentComponent, + componentProps: { + attachments: advanceRequestFileUrlData2, + canEdit: true, + }, + mode: 'ios' as Mode, +}; + +export const modalControllerParams5 = { + component: ViewCommentComponent, + componentProps: { + objectType: 'advance_requests', + objectId: 'areqR1cyLgXdND', + }, + cssClass: 'fy-modal', + showBackdrop: true, + canDismiss: true, + backdropDismiss: true, + animated: true, + initialBreakpoint: 1, + breakpoints: [0, 1], + handle: false, +}; diff --git a/src/app/core/mock-data/unflattened-advance-request.data.ts b/src/app/core/mock-data/unflattened-advance-request.data.ts new file mode 100644 index 0000000000..0d122dcef0 --- /dev/null +++ b/src/app/core/mock-data/unflattened-advance-request.data.ts @@ -0,0 +1,74 @@ +import { UnflattenedAdvanceRequest } from '../models/unflattened-advance-request.model'; + +export const unflattenedAdvanceRequestData: UnflattenedAdvanceRequest = { + areq: { + id: 'areqLFKMxUSAlQ', + created_at: new Date('2023-10-03T09:20:22.112Z'), + approved_at: null, + purpose: 'hello', + notes: 'fdv', + state: 'SUBMITTED', + currency: 'USD', + amount: 2, + org_user_id: 'ouuJzJYWcnzP', + advance_id: null, + policy_amount: null, + policy_flag: null, + policy_state: 'SUCCESS', + project_id: null, + custom_field_values: [ + { + id: 159, + name: 'Advance Request Place', + value: 'd', + type: null, + }, + { + id: 160, + name: 'Category', + value: 'Fyle is best', + type: null, + }, + ], + updated_at: new Date('2023-10-03T14:50:22.552Z'), + source: 'MOBILE', + advance_request_number: 'AR/2023/10/R/1', + updated_by: null, + is_sent_back: null, + is_pulled_back: null, + }, + ou: { + id: 'ouuJzJYWcnzP', + org_id: 'orNbIQloYtfa', + org_name: 'Advance-test', + employee_id: null, + location: null, + level: null, + business_unit: null, + department: null, + title: null, + mobile: null, + sub_department: null, + department_id: null, + }, + us: { + full_name: 'Suyash', + email: 'suyash.p@fyle.in', + name: 'Suyash', + }, + project: { + code: null, + name: null, + }, + advance: { + id: null, + }, + policy: { + amount: null, + flag: null, + state: 'SUCCESS', + }, + new: { + state: 'APPROVAL_PENDING', + }, +}; diff --git a/src/app/core/models/advance-request-delete-params.model.ts b/src/app/core/models/advance-request-delete-params.model.ts new file mode 100644 index 0000000000..54b3f4c2e2 --- /dev/null +++ b/src/app/core/models/advance-request-delete-params.model.ts @@ -0,0 +1,14 @@ +import { FyDeleteDialogComponent } from 'src/app/shared/components/fy-delete-dialog/fy-delete-dialog.component'; +import { AdvanceRequests } from './advance-requests.model'; +import { Observable } from 'rxjs'; + +export interface AdvanceRequestDeleteParams { + component: typeof FyDeleteDialogComponent; + cssClass: string; + backdropDismiss: boolean; + componentProps: { + header: string; + body: string; + deleteMethod: () => Observable; + }; +} diff --git a/src/app/fyle/add-edit-advance-request/add-edit-advance-request-1.page.spec.ts b/src/app/fyle/add-edit-advance-request/add-edit-advance-request-1.page.spec.ts index 550f231692..06fc992ab7 100644 --- a/src/app/fyle/add-edit-advance-request/add-edit-advance-request-1.page.spec.ts +++ b/src/app/fyle/add-edit-advance-request/add-edit-advance-request-1.page.spec.ts @@ -24,7 +24,7 @@ import { } from 'src/app/core/mock-data/add-edit-advance-request-form-value.data'; import { ActivatedRoute, Router } from '@angular/router'; import { expenseFieldsMapResponse } from 'src/app/core/mock-data/expense-fields-map.data'; -import { Observable, of } from 'rxjs'; +import { Observable, Subscription, of, throwError } from 'rxjs'; import { checkPolicyData } from 'src/app/core/mock-data/policy-violation-check.data'; import { advanceRequests } from 'src/app/core/mock-data/advance-requests.data'; import { advRequestFile } from 'src/app/core/mock-data/advance-request-file.data'; @@ -32,7 +32,10 @@ import { fileData1 } from 'src/app/core/mock-data/file.data'; import { PolicyViolationDialogComponent } from './policy-violation-dialog/policy-violation-dialog.component'; import { txnStatusData } from 'src/app/core/mock-data/transaction-status.data'; import { properties } from 'src/app/core/mock-data/modal-properties.data'; -import { advanceRequestPolicyViolationParams } from 'src/app/core/mock-data/modal-controller.data'; +import { + advanceRequestPolicyViolationParams, + popoverControllerParams4, +} from 'src/app/core/mock-data/modal-controller.data'; export function TestCases1(getTestBed) { return describe('test cases 1', () => { @@ -92,10 +95,31 @@ export function TestCases1(getTestBed) { activatedRoute = TestBed.inject(ActivatedRoute) as jasmine.SpyObj; }); + const policyViolationActionDescription = + 'The expense will be flagged, employee will be alerted, expense will be made unreportable and expense amount will be capped to the amount limit.'; + it('should create', () => { expect(component).toBeTruthy(); }); + it('should scroll input into view on keydown', () => { + const inputElement = document.createElement('input'); + spyOn(inputElement, 'scrollIntoView'); + spyOn(component, 'getActiveElement').and.returnValue(inputElement); + + component.scrollInputIntoView(); + + expect(inputElement.scrollIntoView).toHaveBeenCalledWith({ + block: 'center', + }); + }); + + it('getActiveElement(): should return active element in DOM', () => { + const result = component.getActiveElement(); + + expect(result).toEqual(document.activeElement); + }); + it('getFormValues(): should return form value', () => { component.fg = new FormBuilder().group({ ...addEditAdvanceRequestFormValueData }); @@ -241,8 +265,6 @@ export function TestCases1(getTestBed) { component.from = 'TEAM_ADVANCE'; const policyRules = ['rule1', 'rule2']; - const policyViolationActionDescription = - 'The expense will be flagged, employee will be alerted, expense will be made unreportable and expense amount will be capped to the amount limit.'; component.showPolicyModal(policyRules, policyViolationActionDescription, 'draft', advanceRequests); tick(200); @@ -274,8 +296,6 @@ export function TestCases1(getTestBed) { component.from = 'ADVANCE'; const policyRules = ['rule1', 'rule2']; - const policyViolationActionDescription = - 'The expense will be flagged, employee will be alerted, expense will be made unreportable and expense amount will be capped to the amount limit.'; component.showPolicyModal(policyRules, policyViolationActionDescription, 'submit', advanceRequests); tick(200); @@ -306,8 +326,6 @@ export function TestCases1(getTestBed) { modalController.create.and.resolveTo(policyViolationModalSpy); const policyRules = ['rule1', 'rule2']; - const policyViolationActionDescription = - 'The expense will be flagged, employee will be alerted, expense will be made unreportable and expense amount will be capped to the amount limit.'; component.showPolicyModal(policyRules, policyViolationActionDescription, 'draft', advanceRequests); tick(200); @@ -333,8 +351,6 @@ export function TestCases1(getTestBed) { modalController.create.and.resolveTo(policyViolationModalSpy); const policyRules = ['rule1', 'rule2']; - const policyViolationActionDescription = - 'The expense will be flagged, employee will be alerted, expense will be made unreportable and expense amount will be capped to the amount limit.'; component.showPolicyModal(policyRules, policyViolationActionDescription, 'submit', advanceRequests); tick(200); @@ -354,5 +370,132 @@ export function TestCases1(getTestBed) { expect(router.navigate).not.toHaveBeenCalled(); })); }); + + it('showFormValidationErrors(): should show form validation errors', () => { + expenseFieldsService.getAllMap.and.returnValue(of(expenseFieldsMapResponse)); + fixture.detectChanges(); + Object.defineProperty(component.fg, 'valid', { + get: () => false, + }); + spyOn(component.fg, 'markAllAsTouched'); + + component.showFormValidationErrors(); + expect(component.fg.markAllAsTouched).toHaveBeenCalledTimes(1); + }); + + describe('showAdvanceSummaryPopover():', () => { + beforeEach(() => { + expenseFieldsService.getAllMap.and.returnValue(of(expenseFieldsMapResponse)); + fixture.detectChanges(); + spyOn(component, 'save'); + spyOn(component, 'showFormValidationErrors'); + }); + + it('should show advance summary popover and call save method if form is valid', fakeAsync(() => { + Object.defineProperty(component.fg, 'valid', { + get: () => true, + }); + const advanceSummaryPopoverSpy = jasmine.createSpyObj('advanceSummaryPopover', ['present', 'onWillDismiss']); + advanceSummaryPopoverSpy.onWillDismiss.and.resolveTo({ data: { action: 'continue' } }); + popoverController.create.and.resolveTo(advanceSummaryPopoverSpy); + + component.showAdvanceSummaryPopover(); + tick(100); + + expect(popoverController.create).toHaveBeenCalledOnceWith(popoverControllerParams4); + expect(advanceSummaryPopoverSpy.present).toHaveBeenCalledTimes(1); + expect(advanceSummaryPopoverSpy.onWillDismiss).toHaveBeenCalledTimes(1); + expect(component.save).toHaveBeenCalledOnceWith('Draft'); + })); + + it('should call showFormValidationErrors if form is invalid', fakeAsync(() => { + Object.defineProperty(component.fg, 'valid', { + get: () => false, + }); + const advanceSummaryPopoverSpy = jasmine.createSpyObj('advanceSummaryPopover', ['present', 'onWillDismiss']); + advanceSummaryPopoverSpy.onWillDismiss.and.resolveTo({ data: { action: 'continue' } }); + popoverController.create.and.resolveTo(advanceSummaryPopoverSpy); + + component.showAdvanceSummaryPopover(); + tick(100); + + expect(popoverController.create).not.toHaveBeenCalled(); + expect(advanceSummaryPopoverSpy.present).not.toHaveBeenCalled(); + expect(advanceSummaryPopoverSpy.onWillDismiss).not.toHaveBeenCalled(); + expect(component.save).not.toHaveBeenCalled(); + expect(component.showFormValidationErrors).toHaveBeenCalledTimes(1); + })); + }); + + describe('save():', () => { + beforeEach(() => { + spyOn(component, 'generateAdvanceRequestFromFg').and.returnValue(of(advanceRequests)); + spyOn(component, 'checkPolicyViolation').and.returnValue(of(checkPolicyData)); + advanceRequestPolicyService.getPolicyRules.and.returnValue(['rule1', 'rule2']); + spyOn(component, 'showPolicyModal').and.resolveTo(new Subscription()); + spyOn(component, 'saveAndSubmit').and.returnValue(of(advRequestFile)); + component.fg = new FormBuilder().group({}); + }); + + it('should call showPolicyModal if user has come from team advances page, policy rules are present and form is valid', () => { + Object.defineProperty(component.fg, 'valid', { + get: () => true, + }); + component.extendedAdvanceRequest$ = of(advanceRequests); + component.from = 'TEAM_ADVANCE'; + component.save('Draft'); + expect(component.generateAdvanceRequestFromFg).toHaveBeenCalledOnceWith(component.extendedAdvanceRequest$); + expect(component.checkPolicyViolation).toHaveBeenCalledOnceWith(advanceRequests); + expect(advanceRequestPolicyService.getPolicyRules).toHaveBeenCalledOnceWith(checkPolicyData); + expect(component.showPolicyModal).toHaveBeenCalledOnceWith(['rule1', 'rule2'], null, 'draft', advanceRequests); + expect(router.navigate).not.toHaveBeenCalled(); + }); + + it('should navigate to team_advance page if user has come from team advance page, policy rules are not present and form is valid', () => { + Object.defineProperty(component.fg, 'valid', { + get: () => true, + }); + advanceRequestPolicyService.getPolicyRules.and.returnValue([]); + component.extendedAdvanceRequest$ = of(advanceRequests); + component.from = 'TEAM_ADVANCE'; + component.save('Draft'); + expect(component.generateAdvanceRequestFromFg).toHaveBeenCalledOnceWith(component.extendedAdvanceRequest$); + expect(component.checkPolicyViolation).toHaveBeenCalledOnceWith(advanceRequests); + expect(advanceRequestPolicyService.getPolicyRules).toHaveBeenCalledOnceWith(checkPolicyData); + expect(component.showPolicyModal).not.toHaveBeenCalled(); + expect(router.navigate).toHaveBeenCalledOnceWith(['/', 'enterprise', 'team_advance']); + expect(component.saveDraftAdvanceLoading).toBeFalse(); + }); + + it('should navigate to my_advances page if user has come from my advances page, policy rules are not present and form is valid', () => { + Object.defineProperty(component.fg, 'valid', { + get: () => true, + }); + advanceRequestPolicyService.getPolicyRules.and.returnValue([]); + component.extendedAdvanceRequest$ = of(advanceRequests); + component.from = 'ADVANCE'; + component.save('Submit'); + expect(component.generateAdvanceRequestFromFg).toHaveBeenCalledOnceWith(component.extendedAdvanceRequest$); + expect(component.checkPolicyViolation).toHaveBeenCalledOnceWith(advanceRequests); + expect(advanceRequestPolicyService.getPolicyRules).toHaveBeenCalledOnceWith(checkPolicyData); + expect(component.showPolicyModal).not.toHaveBeenCalled(); + expect(router.navigate).toHaveBeenCalledOnceWith(['/', 'enterprise', 'my_advances']); + expect(component.saveAdvanceLoading).toBeFalse(); + }); + + it('should call showFormValidationErrors if form is invalid', () => { + Object.defineProperty(component.fg, 'valid', { + get: () => false, + }); + spyOn(component, 'showFormValidationErrors'); + component.save('Draft'); + expect(component.generateAdvanceRequestFromFg).not.toHaveBeenCalled(); + expect(component.checkPolicyViolation).not.toHaveBeenCalled(); + expect(advanceRequestPolicyService.getPolicyRules).not.toHaveBeenCalled(); + expect(component.showPolicyModal).not.toHaveBeenCalled(); + expect(router.navigate).not.toHaveBeenCalled(); + expect(component.showFormValidationErrors).toHaveBeenCalledTimes(1); + }); + }); }); } diff --git a/src/app/fyle/add-edit-advance-request/add-edit-advance-request-2.page.spec.ts b/src/app/fyle/add-edit-advance-request/add-edit-advance-request-2.page.spec.ts new file mode 100644 index 0000000000..c06c0f412c --- /dev/null +++ b/src/app/fyle/add-edit-advance-request/add-edit-advance-request-2.page.spec.ts @@ -0,0 +1,489 @@ +import { ModalController, PopoverController } from '@ionic/angular'; +import { AdvanceRequestPolicyService } from 'src/app/core/services/advance-request-policy.service'; +import { AdvanceRequestService } from 'src/app/core/services/advance-request.service'; +import { AdvanceRequestsCustomFieldsService } from 'src/app/core/services/advance-requests-custom-fields.service'; +import { AuthService } from 'src/app/core/services/auth.service'; +import { CurrencyService } from 'src/app/core/services/currency.service'; +import { ExpenseFieldsService } from 'src/app/core/services/expense-fields.service'; +import { FileService } from 'src/app/core/services/file.service'; +import { LoaderService } from 'src/app/core/services/loader.service'; +import { ModalPropertiesService } from 'src/app/core/services/modal-properties.service'; +import { NetworkService } from 'src/app/core/services/network.service'; +import { OrgSettingsService } from 'src/app/core/services/org-settings.service'; +import { OrgUserSettingsService } from 'src/app/core/services/org-user-settings.service'; +import { ProjectsService } from 'src/app/core/services/projects.service'; +import { StatusService } from 'src/app/core/services/status.service'; +import { TrackingService } from 'src/app/core/services/tracking.service'; +import { TransactionsOutboxService } from 'src/app/core/services/transactions-outbox.service'; +import { AddEditAdvanceRequestPage } from './add-edit-advance-request.page'; +import { ComponentFixture, discardPeriodicTasks, fakeAsync, tick } from '@angular/core/testing'; +import { FormBuilder, FormControl } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { clone, cloneDeep } from 'lodash'; +import { advanceRequests, advanceRequests2, advanceRequests3 } from 'src/app/core/mock-data/advance-requests.data'; +import { + addEditAdvanceRequestFormValueData, + addEditAdvanceRequestFormValueData3, +} from 'src/app/core/mock-data/add-edit-advance-request-form-value.data'; +import { of } from 'rxjs'; +import { + advanceRequestCustomFieldValuesData, + advanceRequestCustomFieldValuesData2, +} from 'src/app/core/mock-data/advance-request-custom-field-values.data'; +import { + advanceRequestFileUrlData, + advanceRequestFileUrlData2, + expectedFileData2, + fileObject4, + fileObject9, +} from 'src/app/core/mock-data/file-object.data'; +import { fileData3 } from 'src/app/core/mock-data/file.data'; +import { CameraOptionsPopupComponent } from './camera-options-popup/camera-options-popup.component'; +import { CaptureReceiptComponent } from 'src/app/shared/components/capture-receipt/capture-receipt.component'; +import { + modalControllerParams3, + modalControllerParams4, + modalControllerParams5, +} from 'src/app/core/mock-data/modal-controller.data'; +import { properties } from 'src/app/core/mock-data/modal-properties.data'; +import { apiEouRes } from 'src/app/core/mock-data/extended-org-user.data'; +import { orgSettingsRes } from 'src/app/core/mock-data/org-settings.data'; +import { orgUserSettingsData } from 'src/app/core/mock-data/org-user-settings.data'; +import { expenseFieldsMapResponse } from 'src/app/core/mock-data/expense-fields-map.data'; +import { apiAdvanceRequestAction } from 'src/app/core/mock-data/advance-request-actions.data'; +import { unflattenedAdvanceRequestData } from 'src/app/core/mock-data/unflattened-advance-request.data'; +import { projects } from 'src/app/core/mock-data/extended-projects.data'; +import { projectsV1Data } from 'src/app/core/test-data/projects.spec.data'; +import { + advanceRequestCustomFieldData, + advanceRequestCustomFieldData2, +} from 'src/app/core/mock-data/advance-requests-custom-fields.data'; +import { EventEmitter } from '@angular/core'; +import { FyDeleteDialogComponent } from 'src/app/shared/components/fy-delete-dialog/fy-delete-dialog.component'; + +export function TestCases2(getTestBed) { + return describe('test cases 2', () => { + let component: AddEditAdvanceRequestPage; + let fixture: ComponentFixture; + let authService: jasmine.SpyObj; + let advanceRequestsCustomFieldsService: jasmine.SpyObj; + let advanceRequestService: jasmine.SpyObj; + let advanceRequestPolicyService: jasmine.SpyObj; + let modalController: jasmine.SpyObj; + let statusService: jasmine.SpyObj; + let loaderService: jasmine.SpyObj; + let projectsService: jasmine.SpyObj; + let popoverController: jasmine.SpyObj; + let transactionsOutboxService: jasmine.SpyObj; + let fileService: jasmine.SpyObj; + let orgSettingsService: jasmine.SpyObj; + let networkService: jasmine.SpyObj; + let modalProperties: jasmine.SpyObj; + let trackingService: jasmine.SpyObj; + let expenseFieldsService: jasmine.SpyObj; + let currencyService: jasmine.SpyObj; + let orgUserSettingsService: jasmine.SpyObj; + let router: jasmine.SpyObj; + let activatedRoute: jasmine.SpyObj; + + beforeEach(() => { + const TestBed = getTestBed(); + fixture = TestBed.createComponent(AddEditAdvanceRequestPage); + component = fixture.componentInstance; + + authService = TestBed.inject(AuthService) as jasmine.SpyObj; + advanceRequestsCustomFieldsService = TestBed.inject( + AdvanceRequestsCustomFieldsService + ) as jasmine.SpyObj; + advanceRequestService = TestBed.inject(AdvanceRequestService) as jasmine.SpyObj; + advanceRequestPolicyService = TestBed.inject( + AdvanceRequestPolicyService + ) as jasmine.SpyObj; + modalController = TestBed.inject(ModalController) as jasmine.SpyObj; + statusService = TestBed.inject(StatusService) as jasmine.SpyObj; + loaderService = TestBed.inject(LoaderService) as jasmine.SpyObj; + projectsService = TestBed.inject(ProjectsService) as jasmine.SpyObj; + popoverController = TestBed.inject(PopoverController) as jasmine.SpyObj; + transactionsOutboxService = TestBed.inject( + TransactionsOutboxService + ) as jasmine.SpyObj; + fileService = TestBed.inject(FileService) as jasmine.SpyObj; + orgSettingsService = TestBed.inject(OrgSettingsService) as jasmine.SpyObj; + networkService = TestBed.inject(NetworkService) as jasmine.SpyObj; + modalProperties = TestBed.inject(ModalPropertiesService) as jasmine.SpyObj; + trackingService = TestBed.inject(TrackingService) as jasmine.SpyObj; + expenseFieldsService = TestBed.inject(ExpenseFieldsService) as jasmine.SpyObj; + currencyService = TestBed.inject(CurrencyService) as jasmine.SpyObj; + orgUserSettingsService = TestBed.inject(OrgUserSettingsService) as jasmine.SpyObj; + router = TestBed.inject(Router) as jasmine.SpyObj; + activatedRoute = TestBed.inject(ActivatedRoute) as jasmine.SpyObj; + }); + + const policyViolationActionDescription = + 'The expense will be flagged, employee will be alerted, expense will be made unreportable and expense amount will be capped to the amount limit.'; + + it('generateAdvanceRequestFromFg(): should generate advance request from form', () => { + const mockAdvanceRequest = cloneDeep(advanceRequests); + component.fg = new FormBuilder().group(addEditAdvanceRequestFormValueData3); + component.generateAdvanceRequestFromFg(of(mockAdvanceRequest)).subscribe((res) => { + expect(res).toEqual(advanceRequests2); + }); + }); + + it('modifyAdvanceRequestCustomFields(): should sort the values and update the customFieldValues date to YYYY-M-D format if it contains date', () => { + const mockCustomField = cloneDeep(advanceRequestCustomFieldValuesData); + const result = component.modifyAdvanceRequestCustomFields(mockCustomField); + expect(result).toEqual(advanceRequestCustomFieldValuesData2); + expect(component.customFieldValues).toEqual(advanceRequestCustomFieldValuesData2); + }); + + it('fileAttachments(): should return file attachments if ids are absent in fileObject', (done) => { + transactionsOutboxService.fileUpload.and.resolveTo(cloneDeep(fileObject9[0])); + const mockFileObjects = cloneDeep(advanceRequestFileUrlData); + component.dataUrls = mockFileObjects; + component.fileAttachments().subscribe((res) => { + expect(transactionsOutboxService.fileUpload).toHaveBeenCalledOnceWith(undefined, 'image'); + expect(res).toEqual(fileData3); + done(); + }); + }); + + it('addAttachments(): should open camera option popup and add receipt details', fakeAsync(() => { + component.dataUrls = []; + fileService.getImageTypeFromDataUrl.and.returnValue('pdf'); + + const cameraOptionsPopupSpy = jasmine.createSpyObj('cameraOptionsPopup', ['present', 'onWillDismiss']); + cameraOptionsPopupSpy.onWillDismiss.and.resolveTo({ + data: { dataUrl: '2023-02-08/orNVthTo2Zyo/receipts/fi6PQ6z4w6ET.000.pdf', type: 'pdf', option: 'camera' }, + }); + popoverController.create.and.resolveTo(cameraOptionsPopupSpy); + + const captureReceiptModalSpy = jasmine.createSpyObj('captureReceiptModal', ['present', 'onWillDismiss']); + captureReceiptModalSpy.onWillDismiss.and.resolveTo({ + data: { dataUrl: '2023-02-08/orNVthTo2Zyo/receipts/fi6PQ6z4w6ET.000.pdf' }, + }); + modalController.create.and.resolveTo(captureReceiptModalSpy); + + const event = jasmine.createSpyObj('event', ['stopPropagation', 'preventDefault']); + component.addAttachments(event); + tick(100); + + expect(event.stopPropagation).toHaveBeenCalledTimes(1); + expect(event.preventDefault).toHaveBeenCalledTimes(1); + expect(popoverController.create).toHaveBeenCalledOnceWith({ + component: CameraOptionsPopupComponent, + cssClass: 'camera-options-popover', + }); + expect(cameraOptionsPopupSpy.present).toHaveBeenCalledTimes(1); + expect(cameraOptionsPopupSpy.onWillDismiss).toHaveBeenCalledTimes(1); + expect(modalController.create).toHaveBeenCalledOnceWith(modalControllerParams3); + expect(captureReceiptModalSpy.present).toHaveBeenCalledTimes(1); + expect(captureReceiptModalSpy.onWillDismiss).toHaveBeenCalledTimes(1); + expect(fileService.getImageTypeFromDataUrl).toHaveBeenCalledOnceWith( + '2023-02-08/orNVthTo2Zyo/receipts/fi6PQ6z4w6ET.000.pdf' + ); + expect(component.dataUrls).toEqual(expectedFileData2); + })); + + it('viewAttachments(): should show the attachments as preview', fakeAsync(() => { + component.dataUrls = cloneDeep(advanceRequestFileUrlData2); + const attachmentModalSpy = jasmine.createSpyObj('attachmentsModal', ['present', 'onWillDismiss']); + attachmentModalSpy.onWillDismiss.and.resolveTo({ data: { attachments: expectedFileData2 } }); + modalController.create.and.resolveTo(attachmentModalSpy); + + component.viewAttachments(); + tick(100); + expect(modalController.create).toHaveBeenCalledOnceWith(modalControllerParams4); + expect(attachmentModalSpy.present).toHaveBeenCalledTimes(1); + expect(attachmentModalSpy.onWillDismiss).toHaveBeenCalledTimes(1); + expect(component.dataUrls).toEqual(expectedFileData2); + })); + + it('getReceiptExtension(): should return the extension of the receipt', () => { + const mockFileObject = cloneDeep(advanceRequestFileUrlData2[0]); + const result = component.getReceiptExtension(mockFileObject.name); + expect(result).toEqual('pdf'); + }); + + describe('getReceiptDetails():', () => { + it('should return the receipt details with thumbnail as fy-receipt.svg if extension is pdf', () => { + spyOn(component, 'getReceiptExtension').and.returnValue('pdf'); + const mockFileObject = cloneDeep(advanceRequestFileUrlData2[0]); + const result = component.getReceiptDetails(mockFileObject); + expect(component.getReceiptExtension).toHaveBeenCalledOnceWith(mockFileObject.name); + expect(result).toEqual({ + type: 'pdf', + thumbnail: 'img/fy-pdf.svg', + }); + }); + + it('should return the receipt details with type as image and thumbnail as file url if extension is png', () => { + spyOn(component, 'getReceiptExtension').and.returnValue('png'); + const mockFileObject = cloneDeep(advanceRequestFileUrlData2[0]); + const result = component.getReceiptDetails(mockFileObject); + expect(component.getReceiptExtension).toHaveBeenCalledOnceWith(mockFileObject.name); + expect(result).toEqual({ + type: 'image', + thumbnail: + 'https://fyle-storage-mumbai-3.s3.amazonaws.com/2023-02-23/orrjqbDbeP9p/receipts/fiSSsy2Bf4Se.000.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20230223T151537Z&X-Amz-SignedHeaders=host&X-Amz-Expires=604800&X-Amz-Credential=AKIA54Z3LIXTX6CFH4VG%2F20230223%2Fap-south-1%2Fs3%2Faws4_request&X-Amz-Signature=d79c2711892e7cb3f072e223b7b416408c252da38e7df0995e3d256cd8509fee', + }); + }); + }); + + it('getAttachedReceipts(): should return all the attached receipts corresponding to an advance request', () => { + const mockFileObject = cloneDeep(advanceRequestFileUrlData[0]); + spyOn(component, 'getReceiptDetails').and.returnValue({ + type: 'pdf', + thumbnail: 'img/fy-pdf.svg', + }); + fileService.downloadUrl.and.returnValue(of('mockdownloadurl.png')); + fileService.findByAdvanceRequestId.and.returnValue(of([mockFileObject])); + component.dataUrls = [mockFileObject]; + component.getAttachedReceipts('areqR1cyLgXdND').subscribe((res) => { + expect(component.getReceiptDetails).toHaveBeenCalledOnceWith(mockFileObject); + expect(fileService.downloadUrl).toHaveBeenCalledOnceWith('fiSSsy2Bf4Se'); + expect(fileService.findByAdvanceRequestId).toHaveBeenCalledOnceWith('areqR1cyLgXdND'); + expect(res).toEqual([ + { + ...mockFileObject, + type: 'pdf', + thumbnail: 'img/fy-pdf.svg', + url: 'mockdownloadurl.png', + }, + ]); + }); + }); + + describe('openCommentsModal():', () => { + let viewCommentModalSpy: jasmine.SpyObj; + beforeEach(() => { + viewCommentModalSpy = jasmine.createSpyObj('modal', ['present', 'onDidDismiss']); + viewCommentModalSpy.onDidDismiss.and.resolveTo({ data: { updated: true } }); + modalController.create.and.resolveTo(viewCommentModalSpy); + component.id = 'areqR1cyLgXdND'; + modalProperties.getModalDefaultProperties.and.returnValue(properties); + }); + + it('openCommentsModal(): should open the comments modal and call track addComment method if updated is true', fakeAsync(() => { + component.openCommentsModal(); + tick(100); + expect(modalController.create).toHaveBeenCalledOnceWith(modalControllerParams5); + expect(viewCommentModalSpy.present).toHaveBeenCalledTimes(1); + expect(viewCommentModalSpy.onDidDismiss).toHaveBeenCalledTimes(1); + expect(modalProperties.getModalDefaultProperties).toHaveBeenCalledTimes(1); + expect(trackingService.addComment).toHaveBeenCalledTimes(1); + })); + + it('openCommentsModal(): should open the comments modal and call track viewComment method if updated is false', fakeAsync(() => { + viewCommentModalSpy.onDidDismiss.and.resolveTo({ data: { updated: false } }); + modalController.create.and.resolveTo(viewCommentModalSpy); + + component.openCommentsModal(); + tick(100); + expect(modalController.create).toHaveBeenCalledOnceWith(modalControllerParams5); + expect(viewCommentModalSpy.present).toHaveBeenCalledTimes(1); + expect(viewCommentModalSpy.onDidDismiss).toHaveBeenCalledTimes(1); + expect(modalProperties.getModalDefaultProperties).toHaveBeenCalledTimes(1); + expect(trackingService.viewComment).toHaveBeenCalledTimes(1); + })); + }); + + it('getAdvanceRequestDeleteParams(): should return the advance request delete params and deleteMethod should call advanceRequestService.delete', () => { + const props = component.getAdvanceRequestDeleteParams(); + props.componentProps.deleteMethod(); + expect(advanceRequestService.delete).toHaveBeenCalledOnceWith('areqR1cyLgXdND'); + expect(props).toEqual({ + component: FyDeleteDialogComponent, + cssClass: 'delete-dialog', + backdropDismiss: false, + componentProps: { + header: 'Delete Advance Request', + body: 'Are you sure you want to delete this request?', + deleteMethod: jasmine.any(Function), + }, + }); + }); + + it('delete(): should show popover and remove delete advance request', fakeAsync(() => { + spyOn(component, 'getAdvanceRequestDeleteParams'); + const deletePopoverSpy = jasmine.createSpyObj('deletePopover', ['present', 'onDidDismiss']); + deletePopoverSpy.onDidDismiss.and.resolveTo({ data: { status: 'success' } }); + popoverController.create.and.resolveTo(deletePopoverSpy); + advanceRequestService.delete.and.resolveTo(); + component.id = 'areqR1cyLgXdND'; + component.delete(); + tick(100); + expect(popoverController.create).toHaveBeenCalledOnceWith(component.getAdvanceRequestDeleteParams()); + expect(deletePopoverSpy.present).toHaveBeenCalledTimes(1); + expect(deletePopoverSpy.onDidDismiss).toHaveBeenCalledTimes(1); + expect(router.navigate).toHaveBeenCalledOnceWith(['/', 'enterprise', 'my_advances']); + })); + + describe('ionViewWillEnter():', () => { + beforeEach(() => { + expenseFieldsService.getAllMap.and.returnValue(of(expenseFieldsMapResponse)); + authService.getEou.and.resolveTo(apiEouRes); + orgSettingsService.get.and.returnValue(of(orgSettingsRes)); + orgUserSettingsService.get.and.returnValue(of(orgUserSettingsData)); + currencyService.getHomeCurrency.and.returnValue(of('USD')); + advanceRequestService.getActions.and.returnValue(of(apiAdvanceRequestAction)); + loaderService.showLoader.and.resolveTo(undefined); + loaderService.hideLoader.and.resolveTo(undefined); + advanceRequestService.getEReq.and.returnValue(of(unflattenedAdvanceRequestData)); + projectsService.getbyId.and.returnValue(of(projects[0])); + spyOn(component, 'modifyAdvanceRequestCustomFields'); + spyOn(component, 'getAttachedReceipts').and.returnValue(of(fileObject4)); + projectsService.getAllActive.and.returnValue(of(projectsV1Data)); + const mockCustomField = cloneDeep(advanceRequestCustomFieldData); + advanceRequestsCustomFieldsService.getAll.and.returnValue(of(mockCustomField)); + spyOn(component, 'setupNetworkWatcher'); + }); + + it('should set mode, homeCurrency$ and actions$ correctly', fakeAsync(() => { + component.ionViewWillEnter(); + tick(100); + expect(component.mode).toEqual('edit'); + component.homeCurrency$.subscribe((res) => { + expect(currencyService.getHomeCurrency).toHaveBeenCalledTimes(1); + expect(res).toEqual('USD'); + }); + component.actions$.subscribe((res) => { + expect(advanceRequestService.getActions).toHaveBeenCalledOnceWith('areqR1cyLgXdND'); + expect(component.advanceActions).toEqual(apiAdvanceRequestAction); + expect(res).toEqual(apiAdvanceRequestAction); + }); + })); + + it('should set mode to add if id is not defined', fakeAsync(() => { + activatedRoute.snapshot.params = { + id: undefined, + }; + component.ionViewWillEnter(); + tick(100); + expect(component.mode).toEqual('add'); + component.homeCurrency$.subscribe((res) => { + expect(currencyService.getHomeCurrency).toHaveBeenCalledTimes(1); + expect(res).toEqual('USD'); + }); + expect(component.actions$).toBeUndefined(); + })); + + it('should get new advance request observable if mode is add', fakeAsync(() => { + activatedRoute.snapshot.params = { + id: undefined, + }; + component.ionViewWillEnter(); + tick(100); + + component.extendedAdvanceRequest$.subscribe((res) => { + expect(orgSettingsService.get).toHaveBeenCalledTimes(1); + expect(authService.getEou).toHaveBeenCalledTimes(1); + expect(res).toEqual({ ...advanceRequests3, created_at: new Date() }); + }); + })); + + it('should get new advance request observable if mode is add with currency equal to homeCurrency if no preferred_currency is selected', fakeAsync(() => { + const mockOrgUserData = cloneDeep(orgUserSettingsData); + mockOrgUserData.currency_settings.preferred_currency = null; + orgUserSettingsService.get.and.returnValue(of(mockOrgUserData)); + activatedRoute.snapshot.params = { + id: undefined, + }; + component.ionViewWillEnter(); + tick(100); + + component.extendedAdvanceRequest$.subscribe((res) => { + expect(orgSettingsService.get).toHaveBeenCalledTimes(1); + expect(authService.getEou).toHaveBeenCalledTimes(1); + expect(res).toEqual({ ...advanceRequests3, created_at: new Date(), currency: 'USD' }); + }); + })); + + it('should get edit advance request observable if mode is edit', fakeAsync(() => { + activatedRoute.snapshot.params = { + id: 'areqR1cyLgXdND', + }; + const mockAdvanceRequest = cloneDeep(unflattenedAdvanceRequestData); + mockAdvanceRequest.areq.project_id = '3019'; + advanceRequestService.getEReq.and.returnValue(of(mockAdvanceRequest)); + fixture.detectChanges(); + component.ionViewWillEnter(); + tick(100); + component.extendedAdvanceRequest$.subscribe((res) => { + expect(loaderService.showLoader).toHaveBeenCalledTimes(1); + expect(advanceRequestService.getEReq).toHaveBeenCalledOnceWith('areqR1cyLgXdND'); + expect(component.fg.value.currencyObj).toEqual({ + currency: 'USD', + amount: 2, + }); + expect(projectsService.getbyId).toHaveBeenCalledOnceWith('3019'); + expect(component.fg.value.project).toEqual(projects[0]); + expect(component.modifyAdvanceRequestCustomFields).toHaveBeenCalledOnceWith( + mockAdvanceRequest.areq.custom_field_values + ); + expect(component.getAttachedReceipts).toHaveBeenCalledOnceWith('areqR1cyLgXdND'); + expect(component.dataUrls).toEqual(fileObject4); + expect(res).toEqual(mockAdvanceRequest.areq); + }); + })); + + it('should set isProjectsEnabled$, projects$ and isProjectsVisible$ correctly', fakeAsync(() => { + component.ionViewWillEnter(); + tick(100); + component.isProjectsEnabled$.subscribe((res) => { + expect(res).toBeTrue(); + }); + component.projects$.subscribe((res) => { + expect(projectsService.getAllActive).toHaveBeenCalledTimes(1); + expect(res).toEqual(projectsV1Data); + }); + component.isProjectsVisible$.subscribe((res) => { + expect(orgSettingsService.get).toHaveBeenCalledTimes(1); + expect(res).toBeTrue(); + }); + })); + + it('should set isProjectsEnabled$ to false if project_ids are undefined in org user settings data', fakeAsync(() => { + const mockOrgSettingsData = cloneDeep(orgSettingsRes); + mockOrgSettingsData.advanced_projects.enable_individual_projects = true; + orgSettingsService.get.and.returnValue(of(mockOrgSettingsData)); + const mockOrgUserSettingsData = cloneDeep(orgUserSettingsData); + mockOrgUserSettingsData.project_ids = undefined; + orgUserSettingsService.get.and.returnValue(of(mockOrgUserSettingsData)); + component.ionViewWillEnter(); + tick(100); + component.isProjectsVisible$.subscribe((res) => { + expect(res).toBeFalse(); + }); + })); + + it('should set customFields$ correctly', fakeAsync(() => { + const mockCustomField = cloneDeep(advanceRequestCustomFieldData2); + advanceRequestsCustomFieldsService.getAll.and.returnValue(of(mockCustomField)); + const customFieldValuesData = cloneDeep(advanceRequestCustomFieldValuesData); + customFieldValuesData[0].id = 150; + fixture.detectChanges(); + + component.ionViewWillEnter(); + tick(100); + component.customFieldValues = customFieldValuesData; + + expect(component.setupNetworkWatcher).toHaveBeenCalledTimes(1); + component.customFields$.subscribe((res) => { + expect(advanceRequestsCustomFieldsService.getAll).toHaveBeenCalledTimes(1); + expect(res).toEqual(mockCustomField); + }); + })); + }); + + it('setupNetworkWatcher(): should setup network watcher', () => { + networkService.isOnline.and.returnValue(of(false)); + + component.setupNetworkWatcher(); + + expect(networkService.connectivityWatcher).toHaveBeenCalledTimes(1); + expect(networkService.isOnline).toHaveBeenCalledTimes(1); + expect(router.navigate).toHaveBeenCalledOnceWith(['/', 'enterprise', 'my_dashboard']); + }); + }); +} diff --git a/src/app/fyle/add-edit-advance-request/add-edit-advance-request.page.setup.spec.ts b/src/app/fyle/add-edit-advance-request/add-edit-advance-request.page.setup.spec.ts index fe229cc1e1..ac30d5a905 100644 --- a/src/app/fyle/add-edit-advance-request/add-edit-advance-request.page.setup.spec.ts +++ b/src/app/fyle/add-edit-advance-request/add-edit-advance-request.page.setup.spec.ts @@ -23,6 +23,7 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { TestCases1 } from './add-edit-advance-request-1.page.spec'; import { FormBuilder } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; +import { TestCases2 } from './add-edit-advance-request-2.page.spec'; describe('AddEditAdvanceRequestPage', () => { const getTestBed = () => { @@ -42,16 +43,21 @@ describe('AddEditAdvanceRequestPage', () => { const modalControllerSpyObj = jasmine.createSpyObj('ModalController', ['create']); const statusServiceSpyObj = jasmine.createSpyObj('StatusService', ['findLatestComment', 'post']); const loaderServiceSpyObj = jasmine.createSpyObj('LoaderService', ['showLoader', 'hideLoader']); - const projectsServiceSpyObj = jasmine.createSpyObj('ProjectsService', ['getById', 'getAllActive']); + const projectsServiceSpyObj = jasmine.createSpyObj('ProjectsService', ['getbyId', 'getAllActive']); const popoverControllerSpyObj = jasmine.createSpyObj('PopoverController', ['create']); - const transactionsOutboxServiceSpyObj = jasmine.createSpyObj('TransactionsOutboxService', ['addTransaction']); - const fileServiceSpyObj = jasmine.createSpyObj('FileService', ['fileUpload']); + const transactionsOutboxServiceSpyObj = jasmine.createSpyObj('TransactionsOutboxService', ['fileUpload']); + const fileServiceSpyObj = jasmine.createSpyObj('FileService', [ + 'fileUpload', + 'getImageTypeFromDataUrl', + 'downloadUrl', + 'findByAdvanceRequestId', + ]); const orgSettingsServiceSpyObj = jasmine.createSpyObj('OrgSettingsService', ['get']); const networkServiceSpyObj = jasmine.createSpyObj('NetworkService', ['connectivityWatcher', 'isOnline']); const modalPropertiesSpyObj = jasmine.createSpyObj('ModalPropertiesService', ['getModalDefaultProperties']); const trackingServiceSpyObj = jasmine.createSpyObj('TrackingService', ['addComment', 'viewComment']); const expenseFieldsServiceSpyObj = jasmine.createSpyObj('ExpenseFieldsService', ['getAllMap']); - const currencyServiceSpyObj = jasmine.createSpyObj('CurrencyService', ['getHomeCurrrency']); + const currencyServiceSpyObj = jasmine.createSpyObj('CurrencyService', ['getHomeCurrency']); const orgUserSettingsServiceSpyObj = jasmine.createSpyObj('OrgUserSettingsService', ['get']); const routerSpyObj = jasmine.createSpyObj('Router', ['navigate']); @@ -97,4 +103,5 @@ describe('AddEditAdvanceRequestPage', () => { }; TestCases1(getTestBed); + TestCases2(getTestBed); }); diff --git a/src/app/fyle/add-edit-advance-request/add-edit-advance-request.page.ts b/src/app/fyle/add-edit-advance-request/add-edit-advance-request.page.ts index 582d8a2232..a7aa482093 100644 --- a/src/app/fyle/add-edit-advance-request/add-edit-advance-request.page.ts +++ b/src/app/fyle/add-edit-advance-request/add-edit-advance-request.page.ts @@ -39,6 +39,7 @@ import { PolicyViolationCheck } from 'src/app/core/models/policy-violation-check import { AdvanceRequestsCustomFields } from 'src/app/core/models/advance-requests-custom-fields.model'; import { File } from 'src/app/core/models/file.model'; import { AdvanceRequestCustomFieldValues } from 'src/app/core/models/advance-request-custom-field-values.model'; +import { AdvanceRequestDeleteParams } from 'src/app/core/models/advance-request-delete-params.model'; @Component({ selector: 'app-add-edit-advance-request', templateUrl: './add-edit-advance-request.page.html', @@ -113,7 +114,7 @@ export class AddEditAdvanceRequestPage implements OnInit { @HostListener('keydown') scrollInputIntoView(): void { - const el = document.activeElement; + const el = this.getActiveElement(); if (el && el instanceof HTMLInputElement) { el.scrollIntoView({ block: 'center', @@ -121,6 +122,10 @@ export class AddEditAdvanceRequestPage implements OnInit { } } + getActiveElement(): Element { + return document.activeElement; + } + getFormValues(): AddEditAdvanceRequestFormValue { return this.fg.value as AddEditAdvanceRequestFormValue; } @@ -247,6 +252,19 @@ export class AddEditAdvanceRequestPage implements OnInit { }); } + showFormValidationErrors(): void { + this.fg.markAllAsTouched(); + const formContainer = this.formContainer.nativeElement as HTMLElement; + if (formContainer) { + const invalidElement = formContainer.querySelector('.ng-invalid'); + if (invalidElement) { + invalidElement.scrollIntoView({ + behavior: 'smooth', + }); + } + } + } + async showAdvanceSummaryPopover(): Promise { if (this.fg.valid) { const advanceSummaryPopover = await this.popoverController.create({ @@ -275,16 +293,7 @@ export class AddEditAdvanceRequestPage implements OnInit { this.save('Draft'); } } else { - this.fg.markAllAsTouched(); - const formContainer = this.formContainer.nativeElement as HTMLElement; - if (formContainer) { - const invalidElement = formContainer.querySelector('.ng-invalid'); - if (invalidElement) { - invalidElement.scrollIntoView({ - behavior: 'smooth', - }); - } - } + this.showFormValidationErrors(); } } @@ -340,16 +349,7 @@ export class AddEditAdvanceRequestPage implements OnInit { ) .subscribe(noop); } else { - this.fg.markAllAsTouched(); - const formContainer = this.formContainer.nativeElement as HTMLElement; - if (formContainer) { - const invalidElement = formContainer.querySelector('.ng-invalid'); - if (invalidElement) { - invalidElement.scrollIntoView({ - behavior: 'smooth', - }); - } - } + this.showFormValidationErrors(); } } @@ -549,17 +549,22 @@ export class AddEditAdvanceRequestPage implements OnInit { } } - async delete(): Promise { - const deletePopover = await this.popoverController.create({ + getAdvanceRequestDeleteParams(): AdvanceRequestDeleteParams { + return { component: FyDeleteDialogComponent, cssClass: 'delete-dialog', backdropDismiss: false, componentProps: { header: 'Delete Advance Request', body: 'Are you sure you want to delete this request?', - deleteMethod: () => this.advanceRequestService.delete(this.activatedRoute.snapshot.params.id as string), + deleteMethod: (): Observable => + this.advanceRequestService.delete(this.activatedRoute.snapshot.params.id as string), }, - }); + }; + } + + async delete(): Promise { + const deletePopover = await this.popoverController.create(this.getAdvanceRequestDeleteParams()); await deletePopover.present(); @@ -648,7 +653,7 @@ export class AddEditAdvanceRequestPage implements OnInit { ); this.projects$ = this.projectsService.getAllActive(); - this.isProjectsVisible$ = this.orgSettingsService.get().pipe( + this.isProjectsVisible$ = orgSettings$.pipe( switchMap((orgSettings) => iif( () => orgSettings.advanced_projects.enable_individual_projects, From 15f7e10ef1ee2befa840a51a3f8f1f2e4b7288f8 Mon Sep 17 00:00:00 2001 From: Suyash Patil <127177049+suyashpatil78@users.noreply.github.com> Date: Thu, 5 Oct 2023 10:38:47 +0530 Subject: [PATCH 05/14] test: remaining method in add-edit-exp (#2485) --- ...corporate-card-expense-unflattened.data.ts | 12 +++++++++ .../add-edit-expense-6.spec.ts | 27 ++++++++++++++----- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/app/core/mock-data/corporate-card-expense-unflattened.data.ts b/src/app/core/mock-data/corporate-card-expense-unflattened.data.ts index 77eb1110be..0a6f537aa2 100644 --- a/src/app/core/mock-data/corporate-card-expense-unflattened.data.ts +++ b/src/app/core/mock-data/corporate-card-expense-unflattened.data.ts @@ -67,3 +67,15 @@ export const eCCCData2: CCCExpUnflattened = { vendor: null, }, }; + +export const eCCCData3: CCCExpUnflattened = { + ...expectedECccResponse[0], + balance: { + transfer_settlement_id: 'setxPixUhOPVL', + }, + flow: 'newCCCFlow', + ccce: { + ...expectedECccResponse[0].ccce, + corporate_credit_card_account_number: '123456789', + }, +}; diff --git a/src/app/fyle/add-edit-expense/add-edit-expense-6.spec.ts b/src/app/fyle/add-edit-expense/add-edit-expense-6.spec.ts index 134a7fe91b..c07378a827 100644 --- a/src/app/fyle/add-edit-expense/add-edit-expense-6.spec.ts +++ b/src/app/fyle/add-edit-expense/add-edit-expense-6.spec.ts @@ -7,7 +7,11 @@ import { ActivatedRoute, Router } from '@angular/router'; import { ActionSheetController, ModalController, NavController, Platform, PopoverController } from '@ionic/angular'; import { BehaviorSubject, Observable, Subject, Subscription, of } from 'rxjs'; import { AccountType } from 'src/app/core/enums/account-type.enum'; -import { eCCCData2, expectedECccResponse } from 'src/app/core/mock-data/corporate-card-expense-unflattened.data'; +import { + eCCCData2, + eCCCData3, + expectedECccResponse, +} from 'src/app/core/mock-data/corporate-card-expense-unflattened.data'; import { expectedCCdata, expectedCCdata2 } from 'src/app/core/mock-data/cost-centers.data'; import { defaultTxnFieldValuesData3 } from 'src/app/core/mock-data/default-txn-field-values.data'; import { @@ -158,11 +162,11 @@ export function TestCases6(getTestBed) { popupService = TestBed.inject(PopupService) as jasmine.SpyObj; navController = TestBed.inject(NavController) as jasmine.SpyObj; corporateCreditCardExpenseService = TestBed.inject( - CorporateCreditCardExpenseService, + CorporateCreditCardExpenseService ) as jasmine.SpyObj; trackingService = TestBed.inject(TrackingService) as jasmine.SpyObj; recentLocalStorageItemsService = TestBed.inject( - RecentLocalStorageItemsService, + RecentLocalStorageItemsService ) as jasmine.SpyObj; recentlyUsedItemsService = TestBed.inject(RecentlyUsedItemsService) as jasmine.SpyObj; tokenService = TestBed.inject(TokenService) as jasmine.SpyObj; @@ -239,7 +243,7 @@ export function TestCases6(getTestBed) { expect(platformHandlerService.registerBackButtonAction).toHaveBeenCalledOnceWith( BackButtonActionPriority.MEDIUM, - jasmine.any(Function), + jasmine.any(Function) ); expect(dependentFieldSpy.ngOnInit).toHaveBeenCalledTimes(2); }); @@ -254,7 +258,7 @@ export function TestCases6(getTestBed) { expect(platformHandlerService.registerBackButtonAction).toHaveBeenCalledOnceWith( BackButtonActionPriority.MEDIUM, - jasmine.any(Function), + jasmine.any(Function) ); }); }); @@ -347,7 +351,7 @@ export function TestCases6(getTestBed) { 'tax_group_id', 'org_category_id', ], - undefined, + undefined ); })); @@ -512,6 +516,17 @@ export function TestCases6(getTestBed) { expect(component.fg.controls.bus_travel_class.value).toBeNull(); })); + it('initCCCTxn(): should initialize CCC txn and initialize card number and ending digits', () => { + activatedRoute.snapshot.params = { + bankTxn: JSON.stringify(eCCCData3), + }; + component.initCCCTxn(); + expect(component.showSelectedTransaction).toBeTrue(); + expect(component.selectedCCCTransaction).toEqual(eCCCData3.ccce); + expect(component.isCreatedFromCCC).toBeTrue(); + expect(component.cardEndingDigits).toEqual('6789'); + }); + it('ngOnInit(): should populate report permissions', () => { activatedRoute.snapshot.params.remove_from_report = JSON.stringify(true); fixture.detectChanges(); From ab028c12bba67681391767ef52a26007d13f575f Mon Sep 17 00:00:00 2001 From: jayfyle <115472256+jayfyle@users.noreply.github.com> Date: Thu, 5 Oct 2023 13:08:41 +0530 Subject: [PATCH 06/14] fix: Notifications Page Fixing Tests (#2487) * fixed tests * fixing tests -2 --------- Co-authored-by: Jay Budhadev --- .../mock-data/notification-events.data.ts | 390 ++++++++++++++++++ .../org-user-settings.service.spec.ts | 5 +- .../notifications/notifications.page.spec.ts | 8 +- 3 files changed, 397 insertions(+), 6 deletions(-) diff --git a/src/app/core/mock-data/notification-events.data.ts b/src/app/core/mock-data/notification-events.data.ts index 91b532b856..85d0b635ee 100644 --- a/src/app/core/mock-data/notification-events.data.ts +++ b/src/app/core/mock-data/notification-events.data.ts @@ -194,3 +194,393 @@ export const notificationEventsData: NotificationEvents = { }, ], }; + +export const notificationEventsData2: NotificationEvents = { + features: { + expensesAndReports: { + textLabel: 'Expenses and Reports', + selected: true, + }, + advances: { + textLabel: 'Advances', + selected: true, + }, + }, + events: [ + { + textLabel: 'When an expense is created via email', + selected: true, + email: { + selected: true, + }, + push: { + selected: true, + }, + feature: 'expensesAndReports', + eventType: 'eous_forward_email_to_user', + }, + { + textLabel: 'On submission of expense report', + selected: true, + email: { + selected: true, + }, + push: { + selected: true, + }, + feature: 'expensesAndReports', + eventType: 'erpts_submitted', + }, + { + textLabel: 'When a comment is left on an expense', + selected: true, + email: { + selected: true, + }, + push: { + selected: true, + }, + feature: 'expensesAndReports', + eventType: 'estatuses_created_txn', + }, + { + textLabel: 'When a comment is left on a report', + selected: true, + email: { + selected: true, + }, + push: { + selected: true, + }, + feature: 'expensesAndReports', + eventType: 'estatuses_created_rpt', + }, + { + textLabel: 'When an expense is removed', + selected: true, + email: { + selected: true, + }, + push: { + selected: true, + }, + feature: 'expensesAndReports', + eventType: 'etxns_admin_removed', + }, + { + textLabel: 'When an expense is edited by someone else', + selected: true, + email: { + selected: true, + }, + push: { + selected: true, + }, + feature: 'expensesAndReports', + eventType: 'etxns_admin_updated', + }, + { + textLabel: 'When a reported expense is sent back', + selected: true, + email: { + selected: true, + }, + push: { + selected: true, + }, + feature: 'expensesAndReports', + eventType: 'erpts_inquiry', + }, + { + textLabel: 'When a report is approved', + selected: true, + email: { + selected: true, + }, + push: { + selected: true, + }, + feature: 'expensesAndReports', + eventType: 'erpts_approved', + }, + { + textLabel: 'When a reimbursement is done', + selected: true, + email: { + selected: true, + }, + push: { + selected: true, + }, + feature: 'expensesAndReports', + eventType: 'ereimbursements_completed', + }, + { + textLabel: 'When an advance request is submitted', + selected: true, + email: { + selected: true, + }, + push: { + selected: true, + }, + feature: 'advances', + eventType: 'eadvance_requests_created', + }, + { + textLabel: 'When an advance request is updated', + selected: true, + email: { + selected: true, + }, + push: { + selected: true, + }, + feature: 'advances', + eventType: 'eadvance_requests_updated', + }, + { + textLabel: 'When an advance request is sent back', + selected: true, + email: { + selected: true, + }, + push: { + selected: true, + }, + feature: 'advances', + eventType: 'eadvance_requests_inquiry', + }, + { + textLabel: 'When an advance request is approved', + selected: true, + email: { + selected: true, + }, + push: { + selected: true, + }, + feature: 'advances', + eventType: 'eadvance_requests_approved', + }, + { + textLabel: 'When an advance is assigned', + selected: true, + email: { + selected: true, + }, + push: { + selected: true, + }, + feature: 'advances', + eventType: 'eadvances_created', + }, + { + textLabel: 'When an advance request is rejected', + selected: true, + email: { + selected: true, + }, + push: { + selected: true, + }, + feature: 'advances', + eventType: 'eadvance_requests_rejected', + }, + ], +}; + +export const notificationEventsData3: NotificationEvents = { + features: { + expensesAndReports: { + textLabel: 'Expenses and Reports', + selected: true, + }, + advances: { + textLabel: 'Advances', + selected: true, + }, + }, + events: [ + { + textLabel: 'When an expense is created via email', + selected: true, + email: { + selected: true, + }, + push: { + selected: true, + }, + feature: 'expensesAndReports', + eventType: 'eous_forward_email_to_user', + }, + { + textLabel: 'On submission of expense report', + selected: true, + email: { + selected: true, + }, + push: { + selected: true, + }, + feature: 'expensesAndReports', + eventType: 'erpts_submitted', + }, + { + textLabel: 'When a comment is left on an expense', + selected: true, + email: { + selected: true, + }, + push: { + selected: true, + }, + feature: 'expensesAndReports', + eventType: 'estatuses_created_txn', + }, + { + textLabel: 'When a comment is left on a report', + selected: true, + email: { + selected: true, + }, + push: { + selected: true, + }, + feature: 'expensesAndReports', + eventType: 'estatuses_created_rpt', + }, + { + textLabel: 'When an expense is removed', + selected: true, + email: { + selected: true, + }, + push: { + selected: true, + }, + feature: 'expensesAndReports', + eventType: 'etxns_admin_removed', + }, + { + textLabel: 'When an expense is edited by someone else', + selected: true, + email: { + selected: true, + }, + push: { + selected: true, + }, + feature: 'expensesAndReports', + eventType: 'etxns_admin_updated', + }, + { + textLabel: 'When a reported expense is sent back', + selected: true, + email: { + selected: true, + }, + push: { + selected: true, + }, + feature: 'expensesAndReports', + eventType: 'erpts_inquiry', + }, + { + textLabel: 'When a report is approved', + selected: true, + email: { + selected: true, + }, + push: { + selected: true, + }, + feature: 'expensesAndReports', + eventType: 'erpts_approved', + }, + { + textLabel: 'When a reimbursement is done', + selected: true, + email: { + selected: true, + }, + push: { + selected: true, + }, + feature: 'expensesAndReports', + eventType: 'ereimbursements_completed', + }, + { + textLabel: 'When an advance request is submitted', + selected: true, + email: { + selected: true, + }, + push: { + selected: true, + }, + feature: 'advances', + eventType: 'eadvance_requests_created', + }, + { + textLabel: 'When an advance request is updated', + selected: true, + email: { + selected: true, + }, + push: { + selected: true, + }, + feature: 'advances', + eventType: 'eadvance_requests_updated', + }, + { + textLabel: 'When an advance request is sent back', + selected: true, + email: { + selected: true, + }, + push: { + selected: true, + }, + feature: 'advances', + eventType: 'eadvance_requests_inquiry', + }, + { + textLabel: 'When an advance request is approved', + selected: true, + email: { + selected: true, + }, + push: { + selected: true, + }, + feature: 'advances', + eventType: 'eadvance_requests_approved', + }, + { + textLabel: 'When an advance is assigned', + selected: true, + email: { + selected: true, + }, + push: { + selected: true, + }, + feature: 'advances', + eventType: 'eadvances_created', + }, + { + textLabel: 'When an advance request is rejected', + selected: true, + email: { + selected: true, + }, + push: { + selected: true, + }, + feature: 'advances', + eventType: 'eadvance_requests_rejected', + }, + ], +}; diff --git a/src/app/core/services/org-user-settings.service.spec.ts b/src/app/core/services/org-user-settings.service.spec.ts index aee416195c..a6dba151b8 100644 --- a/src/app/core/services/org-user-settings.service.spec.ts +++ b/src/app/core/services/org-user-settings.service.spec.ts @@ -11,8 +11,9 @@ import { } from '../mock-data/org-user-settings.data'; import { currentEouUnflatted } from '../test-data/org-user.service.spec.data'; import { emailEvents } from '../mock-data/email-events.data'; -import { notificationEventsData } from '../mock-data/notification-events.data'; +import { notificationEventsData, notificationEventsData3 } from '../mock-data/notification-events.data'; import { costCentersData2, costCentersData3 } from '../mock-data/cost-centers.data'; +import { cloneDeep } from 'lodash'; describe('OrgUserSettingsService', () => { let orgUserSettingsService: OrgUserSettingsService; @@ -95,7 +96,7 @@ describe('OrgUserSettingsService', () => { it('getNotificationEvents(): should get notification events', (done) => { orgUserSettingsService.getNotificationEvents().subscribe((res) => { - expect(res).toEqual(notificationEventsData); + expect(res).toEqual(cloneDeep(notificationEventsData3)); done(); }); }); diff --git a/src/app/fyle/notifications/notifications.page.spec.ts b/src/app/fyle/notifications/notifications.page.spec.ts index 82e8a21746..8a3f6810d9 100644 --- a/src/app/fyle/notifications/notifications.page.spec.ts +++ b/src/app/fyle/notifications/notifications.page.spec.ts @@ -8,7 +8,7 @@ import { RouterTestingModule } from '@angular/router/testing'; import { cloneDeep } from 'lodash'; import { of } from 'rxjs'; import { apiEouRes } from 'src/app/core/mock-data/extended-org-user.data'; -import { notificationEventsData } from 'src/app/core/mock-data/notification-events.data'; +import { notificationEventsData, notificationEventsData2 } from 'src/app/core/mock-data/notification-events.data'; import { orgSettingsWithUnsubscribeEvent } from 'src/app/core/mock-data/org-settings.data'; import { notificationDelegateeSettings1, @@ -182,12 +182,12 @@ describe('NotificationsPage', () => { it('removeAdminUnsbscribedEvents(): should remove admin unsubscribe events', fakeAsync(() => { component.orgSettings$ = of(orgSettingsWithUnsubscribeEvent); component.orgSettings = orgSettingsWithUnsubscribeEvent; - component.notificationEvents = cloneDeep(notificationEventsData); + component.notificationEvents = cloneDeep(notificationEventsData2); component.removeAdminUnsbscribedEvents(); tick(500); - expect(Object.keys(component.notificationEvents.features).includes('advances')).toBeFalse(); + expect(Object.keys(component.notificationEvents.features)).toEqual(['expensesAndReports', 'advances']); })); it('updateAdvanceRequestFeatures(): should update advance request features', () => { @@ -236,7 +236,7 @@ describe('NotificationsPage', () => { it('removeDisabledFeatures(): should remove disabled features', () => { spyOn(component, 'updateAdvanceRequestFeatures'); - component.notificationEvents = cloneDeep(notificationEventsData); + component.notificationEvents = cloneDeep(notificationEventsData2); component.removeDisabledFeatures(); From 21c502511fb4f302ef33441183d7117e7fba67ec Mon Sep 17 00:00:00 2001 From: Suyash Patil <127177049+suyashpatil78@users.noreply.github.com> Date: Thu, 5 Oct 2023 16:51:08 +0530 Subject: [PATCH 07/14] feat: Updating readme (#2491) * Updating readme * minor --- README.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 424746af45..edbcc3a92b 100644 --- a/README.md +++ b/README.md @@ -12,18 +12,19 @@ Please install node v14.17.2 or above via nvm. - ionic serve -c `env_name` # IMPORTANT -## before making any changes - - - go to .git/hooks - - run in the shell - chmod +x pre-commit - - Note: It is to prevent keys from accidentally leaking. - - ## For setting environment variables - Ping mobile app team for environment files - Add them inside the environments folder - - Note: Do not make any changes to the environment.ts file - this is a template folder for creating configurations. + - Note: Do not make any changes to the environment.ts file - this is a template folder for creating configurations. Also, make sure not to put staging envs in this file. + - If you are getting errors like this: + ``` + Property 'LIVE_UPDATE_APP_VERSION' does not exist on type + '{ production: boolean; NAME: string; CLUSTER_DOMAIN: string; ROOT_URL: string; ROUTER_API_ENDPOINT: string; + ANDROID_CLIENT_ID: string; IP_FIND_KEY: string; GOOGLE_MAPS_API_KEY: string; FRESHCHAT_TOKEN: string; + SENTRY_DSN: string; REFINER_NPS_FORM_ID: string; }' + ``` + make sure you have the latest `environment.staging.ts` file. ## For creating pull requests From 0fde8ed786870eeb7d5884b72446c04f49992833 Mon Sep 17 00:00:00 2001 From: Omkar Joshi <65808188+OmkarJ13@users.noreply.github.com> Date: Mon, 9 Oct 2023 11:54:17 +0530 Subject: [PATCH 08/14] fix: Add card CTA usability issue (#2497) --- src/app/shared/components/add-card/add-card.component.html | 6 +++--- .../shared/components/add-card/add-card.component.spec.ts | 6 +++--- .../components/spent-cards/spent-cards.component.html | 1 + 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/app/shared/components/add-card/add-card.component.html b/src/app/shared/components/add-card/add-card.component.html index 4b11e2342d..dbaec135d9 100644 --- a/src/app/shared/components/add-card/add-card.component.html +++ b/src/app/shared/components/add-card/add-card.component.html @@ -1,9 +1,9 @@ -
+
No card added yet! - +
diff --git a/src/app/shared/components/add-card/add-card.component.spec.ts b/src/app/shared/components/add-card/add-card.component.spec.ts index 3464e34951..c549961fb3 100644 --- a/src/app/shared/components/add-card/add-card.component.spec.ts +++ b/src/app/shared/components/add-card/add-card.component.spec.ts @@ -42,11 +42,11 @@ describe('AddCardComponent', () => { expect(zeroStateMessageElement).toBeNull(); }); - it('should raise an event addCardClick when the add card button is clicked', () => { + it('should raise an event addCardClick when the add card container is clicked', () => { spyOn(component.addCardClick, 'emit'); - const addCardButton = getElementRef(fixture, '[data-testid="add-card-button"]'); - addCardButton.nativeElement.click(); + const addCardContainer = getElementRef(fixture, '[data-testid="add-card-container"]'); + addCardContainer.nativeElement.click(); expect(component.addCardClick.emit).toHaveBeenCalled(); }); diff --git a/src/app/shared/components/spent-cards/spent-cards.component.html b/src/app/shared/components/spent-cards/spent-cards.component.html index 47a2ecc75c..3c3012ac14 100644 --- a/src/app/shared/components/spent-cards/spent-cards.component.html +++ b/src/app/shared/components/spent-cards/spent-cards.component.html @@ -6,6 +6,7 @@ [slidesPerView]="1.1" [spaceBetween]="16" [pagination]="pagination" + [centeredSlides]="cardDetails.length === 1 && !showAddCardSlide" > From 5357414c60758dd3838d5d874c669bc92891dd25 Mon Sep 17 00:00:00 2001 From: hlkavya0213 <79827230+hlkavya0213@users.noreply.github.com> Date: Mon, 9 Oct 2023 18:02:20 +0530 Subject: [PATCH 09/14] feat: remove thumbnails across mobile app (#2499) --- src/app/core/mock-data/file-object.data.ts | 3 +- src/app/core/services/file.service.spec.ts | 41 +----- src/app/core/services/file.service.ts | 28 +---- .../split-expense/split-expense.page.spec.ts | 4 +- .../expense-card-lite.component.html | 14 +-- .../expense-card-lite.component.scss | 7 +- .../expense-card-lite.component.spec.ts | 69 ++++------ .../expense-card-lite.component.ts | 21 +--- .../expenses-card.component.html | 17 +-- .../expenses-card.component.scss | 7 +- .../expenses-card.component.spec.ts | 118 +----------------- .../expenses-card/expenses-card.component.ts | 61 ++------- src/assets/images/pdf-receipt-placeholder.png | Bin 0 -> 7933 bytes 13 files changed, 77 insertions(+), 313 deletions(-) create mode 100644 src/assets/images/pdf-receipt-placeholder.png diff --git a/src/app/core/mock-data/file-object.data.ts b/src/app/core/mock-data/file-object.data.ts index 8655f0fd8f..b6f37c79d9 100644 --- a/src/app/core/mock-data/file-object.data.ts +++ b/src/app/core/mock-data/file-object.data.ts @@ -78,10 +78,9 @@ export const fileObjectData5: FileObject = { purpose: '', }; -export const thumbnailUrlMockData: FileObject[] = [ +export const fileUrlMockData: FileObject[] = [ { id: 'fiwJ0nQTBpYH', - purpose: 'THUMBNAILx200x200', url: 'mock-url-1', }, ]; diff --git a/src/app/core/services/file.service.spec.ts b/src/app/core/services/file.service.spec.ts index 15cf211c74..11d50a46de 100644 --- a/src/app/core/services/file.service.spec.ts +++ b/src/app/core/services/file.service.spec.ts @@ -1,12 +1,6 @@ import { TestBed } from '@angular/core/testing'; import { of } from 'rxjs'; -import { - fileObjectAdv, - fileObjectAdv1, - fileObjectData, - fileObjectData4, - thumbnailUrlMockData, -} from '../mock-data/file-object.data'; +import { fileObjectAdv, fileObjectAdv1, fileObjectData, fileObjectData4 } from '../mock-data/file-object.data'; import { ApiService } from './api.service'; import { DateService } from './date.service'; @@ -42,39 +36,6 @@ describe('FileService', () => { expect(fileService).toBeTruthy(); }); - it('downloadThumbnailUrl(): should return the file obj with thumbnail url', (done) => { - apiService.post.and.returnValue(of(thumbnailUrlMockData)); - - const fileId = 'fiwJ0nQTBpYH'; - fileService.downloadThumbnailUrl(fileId).subscribe((res) => { - expect(res).toEqual(thumbnailUrlMockData); - expect(apiService.post).toHaveBeenCalledOnceWith('/files/download_urls', [ - { - id: fileId, - purpose: 'THUMBNAILx200x200', - }, - ]); - done(); - }); - }); - - it('getFilesWithThumbnail(): should return files with thumbnail for the given txn ID', (done) => { - apiService.get.and.returnValue(of([fileObjectData])); - - const txnId = 'txdzGV1TZEg3'; - fileService.getFilesWithThumbnail(txnId).subscribe((res) => { - expect(res).toEqual([fileObjectData]); - expect(apiService.get).toHaveBeenCalledOnceWith('/files', { - params: { - transaction_id: txnId, - skip_html: 'true', - purpose: 'THUMBNAILx200x200', - }, - }); - done(); - }); - }); - it('base64Download(): should return the base64 encoded file content', (done) => { apiService.get.and.returnValue(of({ content: 'base64encodedcontent' })); diff --git a/src/app/core/services/file.service.ts b/src/app/core/services/file.service.ts index 937437af53..225c8c7d80 100644 --- a/src/app/core/services/file.service.ts +++ b/src/app/core/services/file.service.ts @@ -12,34 +12,12 @@ import { DateService } from './date.service'; providedIn: 'root', }) export class FileService { - constructor( - private apiService: ApiService, - private dateService: DateService, - ) {} + constructor(private apiService: ApiService, private dateService: DateService) {} downloadUrl(fileId: string): Observable { return this.apiService.post('/files/' + fileId + '/download_url').pipe(map((res) => res.url)); } - downloadThumbnailUrl(fileId: string): Observable { - return this.apiService.post('/files/download_urls', [ - { - id: fileId, - purpose: 'THUMBNAILx200x200', - }, - ]); - } - - getFilesWithThumbnail(txnId: string): Observable { - return this.apiService.get('/files', { - params: { - transaction_id: txnId, - skip_html: 'true', - purpose: 'THUMBNAILx200x200', - }, - }); - } - base64Download(fileId: string): Observable<{ content: string }> { return this.apiService.get('/files/' + fileId + '/download_b64'); } @@ -51,7 +29,7 @@ export class FileService { advance_request_id: advanceRequestId, skip_html: 'true', }, - }), + }) ).pipe( map((files) => { files.map((file) => { @@ -59,7 +37,7 @@ export class FileService { this.setFileType(file as FileObject); }); return files as unknown as FileObject[]; - }), + }) ); } diff --git a/src/app/fyle/split-expense/split-expense.page.spec.ts b/src/app/fyle/split-expense/split-expense.page.spec.ts index 492730b5f3..e7ac7feaef 100644 --- a/src/app/fyle/split-expense/split-expense.page.spec.ts +++ b/src/app/fyle/split-expense/split-expense.page.spec.ts @@ -84,7 +84,7 @@ import { splitExpFile2, splitExpFile3, splitExpFileObj, - thumbnailUrlMockData, + fileUrlMockData, fileObjectData1, } from 'src/app/core/mock-data/file-object.data'; import { @@ -632,7 +632,7 @@ describe('SplitExpensePage', () => { it('should return the attached files when the transaction id is specified', (done) => { spyOn(component, 'getAttachedFiles').and.returnValue(of(fileObject8)); - const FileObject9: FileObject[] = thumbnailUrlMockData.map((fileObject) => ({ + const FileObject9: FileObject[] = fileUrlMockData.map((fileObject) => ({ ...fileObject, id: 'fizBwnXhyZTp', })); diff --git a/src/app/shared/components/expense-card-lite/expense-card-lite.component.html b/src/app/shared/components/expense-card-lite/expense-card-lite.component.html index ae9a29ff24..afce935eb9 100644 --- a/src/app/shared/components/expense-card-lite/expense-card-lite.component.html +++ b/src/app/shared/components/expense-card-lite/expense-card-lite.component.html @@ -1,6 +1,6 @@
- +
-
+
+ +
@@ -29,7 +25,7 @@
- {{ expense.txn_dt | date: 'MMM dd, YYYY' }} + {{ expense.txn_dt | date : 'MMM dd, YYYY' }}
diff --git a/src/app/shared/components/expense-card-lite/expense-card-lite.component.scss b/src/app/shared/components/expense-card-lite/expense-card-lite.component.scss index 6a59195d44..75507b99af 100644 --- a/src/app/shared/components/expense-card-lite/expense-card-lite.component.scss +++ b/src/app/shared/components/expense-card-lite/expense-card-lite.component.scss @@ -28,7 +28,7 @@ padding: 15px; } - &--receipt-thumbnail-container { + &--receipt-image-container { background-color: $light-grey; border-radius: 8px; height: 64px; @@ -38,6 +38,11 @@ align-items: center; } + &--receipt-image { + height: 60px; + width: 80px; + } + &--with-image { background-size: cover; background-repeat: no-repeat; diff --git a/src/app/shared/components/expense-card-lite/expense-card-lite.component.spec.ts b/src/app/shared/components/expense-card-lite/expense-card-lite.component.spec.ts index 009a8fbe0f..bcb7a45cd2 100644 --- a/src/app/shared/components/expense-card-lite/expense-card-lite.component.spec.ts +++ b/src/app/shared/components/expense-card-lite/expense-card-lite.component.spec.ts @@ -1,22 +1,13 @@ -import { ComponentFixture, TestBed, async, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { FileService } from 'src/app/core/services/file.service'; import { ExpenseCardLiteComponent } from './expense-card-lite.component'; import { IonicModule } from '@ionic/angular'; import { MatIconModule } from '@angular/material/icon'; import { MatIconTestingModule } from '@angular/material/icon/testing'; import { CurrencySymbolPipe } from '../../pipes/currency-symbol.pipe'; -import { fileObjectData1 } from 'src/app/core/mock-data/file-object.data'; -import { of } from 'rxjs'; import { getElementBySelector, getTextContent } from 'src/app/core/dom-helpers'; -import { FileObject } from 'src/app/core/models/file-obj.model'; - -const thumbnailUrlMockData1: FileObject[] = [ - { - id: 'fiwJ0nQTBpYH', - purpose: 'THUMBNAILx200x200', - url: '/assets/images/add-to-list.png', - }, -]; +import { of } from 'rxjs'; +import { fileObjectData } from 'src/app/core/mock-data/file-object.data'; describe('ExpenseCardLiteComponent', () => { let expenseCardLiteComponent: ExpenseCardLiteComponent; @@ -24,9 +15,7 @@ describe('ExpenseCardLiteComponent', () => { let fileService: jasmine.SpyObj; beforeEach(waitForAsync(() => { - const fileServiceSpy = jasmine.createSpyObj('FileService', ['getFilesWithThumbnail', 'downloadThumbnailUrl']); - fileServiceSpy.getFilesWithThumbnail.and.returnValue(of(fileObjectData1)); - fileServiceSpy.downloadThumbnailUrl.and.returnValue(of(thumbnailUrlMockData1)); + const fileServiceSpy = jasmine.createSpyObj('FileService', ['findByTransactionId']); TestBed.configureTestingModule({ declarations: [ExpenseCardLiteComponent, CurrencySymbolPipe], @@ -42,58 +31,48 @@ describe('ExpenseCardLiteComponent', () => { fixture = TestBed.createComponent(ExpenseCardLiteComponent); expenseCardLiteComponent = fixture.componentInstance; - const expense = { id: 'txn1234' }; - expenseCardLiteComponent.expense = expense; - fixture.detectChanges(); })); it('should create', () => { expect(expenseCardLiteComponent).toBeTruthy(); }); - it('should call getReceipt on initialization', () => { - spyOn(expenseCardLiteComponent, 'getReceipt'); - expenseCardLiteComponent.ngOnInit(); - expect(expenseCardLiteComponent.getReceipt).toHaveBeenCalledTimes(1); - }); + const initialSetup = (fileData) => { + fileService.findByTransactionId.and.returnValue(of(fileData)); + expenseCardLiteComponent.expense = { id: 'txn1234' }; + fixture.detectChanges(); + }; describe('getReceipt():', () => { - it('should set receiptThumbnail if there is at least one thumbnail file', () => { - fixture.detectChanges(); - expect(fileService.getFilesWithThumbnail).toHaveBeenCalledOnceWith(expenseCardLiteComponent.expense.id); - expect(fileService.downloadThumbnailUrl).toHaveBeenCalledOnceWith(fileObjectData1[0].id); - expect(expenseCardLiteComponent.receiptThumbnail).toEqual(thumbnailUrlMockData1[0].url); + it('should set isReceiptPresent to true when files are present', () => { + initialSetup([fileObjectData]); + expect(fileService.findByTransactionId).toHaveBeenCalledWith('txn1234'); + expect(expenseCardLiteComponent.isReceiptPresent).toBeTruthy(); }); - it('should return an empty array if there are no thumbnail files', () => { - fileService.getFilesWithThumbnail.and.returnValue(of([])); - fixture.detectChanges(); - expenseCardLiteComponent.getReceipt(); - expect(fileService.getFilesWithThumbnail).toHaveBeenCalledWith(expenseCardLiteComponent.expense.id); - expect(expenseCardLiteComponent.receiptThumbnail).toEqual(thumbnailUrlMockData1[0].url); + it('should set isReceiptPresent to false when no files are present', () => { + initialSetup([]); + expect(fileService.findByTransactionId).toHaveBeenCalledWith('txn1234'); + expect(expenseCardLiteComponent.isReceiptPresent).toBeFalsy(); }); }); - it('should display the thumbnail when available', () => { - fixture.detectChanges(); + it('should display the receipt when available', () => { + initialSetup([fileObjectData]); const element = fixture.nativeElement; - const thumbnailContainer = element.querySelector('.expenses-card--receipt-thumbnail-container'); - const backgroundImage = thumbnailContainer.style.backgroundImage; - expect(thumbnailContainer).toBeTruthy(); - expect(backgroundImage).toContain(expenseCardLiteComponent.receiptThumbnail); + const receiptContainer = element.querySelector('.expenses-card--receipt-image-container'); + expect(receiptContainer).toBeTruthy(); }); - it('should display a default icon when no thumbnail available', () => { - expenseCardLiteComponent.receiptThumbnail = null; - fixture.detectChanges(); - + it('should display a default icon when no receipt available', () => { + initialSetup([]); const element = fixture.nativeElement; const icon = element.querySelector('.expenses-card--receipt-icon'); expect(icon).toBeTruthy(); }); it('should display "Unspecified" if purpose is not present', () => { - fixture.detectChanges(); + initialSetup([]); const purpose = getElementBySelector(fixture, '.expenses-card--category'); expect(getTextContent(purpose)).toEqual('Unspecified'); }); diff --git a/src/app/shared/components/expense-card-lite/expense-card-lite.component.ts b/src/app/shared/components/expense-card-lite/expense-card-lite.component.ts index d24e3763e8..22500e4ec2 100644 --- a/src/app/shared/components/expense-card-lite/expense-card-lite.component.ts +++ b/src/app/shared/components/expense-card-lite/expense-card-lite.component.ts @@ -1,6 +1,4 @@ import { Component, Input, OnInit } from '@angular/core'; -import { noop } from 'rxjs'; -import { map, switchMap } from 'rxjs/operators'; import { FileObject } from 'src/app/core/models/file-obj.model'; import { FileService } from 'src/app/core/services/file.service'; @@ -12,7 +10,7 @@ import { FileService } from 'src/app/core/services/file.service'; export class ExpenseCardLiteComponent implements OnInit { @Input() expense; - receiptThumbnail: string; + isReceiptPresent: boolean; constructor(private fileService: FileService) {} @@ -21,19 +19,8 @@ export class ExpenseCardLiteComponent implements OnInit { } getReceipt() { - this.fileService - .getFilesWithThumbnail(this.expense.id) - .pipe( - switchMap((ThumbFiles: FileObject[]) => { - if (ThumbFiles.length > 0) { - return this.fileService.downloadThumbnailUrl(ThumbFiles[0].id); - } else { - return []; - } - }) - ) - .subscribe((downloadUrl: FileObject[]) => { - this.receiptThumbnail = downloadUrl[0].url; - }); + this.fileService.findByTransactionId(this.expense.id).subscribe((files: FileObject[]) => { + this.isReceiptPresent = files.length > 0; + }); } } diff --git a/src/app/shared/components/expenses-card/expenses-card.component.html b/src/app/shared/components/expenses-card/expenses-card.component.html index 99c560f7f0..28bc75e0b3 100644 --- a/src/app/shared/components/expenses-card/expenses-card.component.html +++ b/src/app/shared/components/expenses-card/expenses-card.component.html @@ -26,7 +26,7 @@
- +
+
@@ -62,7 +63,9 @@ class="expenses-card--receipt-container expenses-card--with-image" *ngIf="attachmentUploadInProgress" [ngStyle]="{ - 'background-image': inlineReceiptDataUrl && imageTransperencyOverlay + 'url(' + inlineReceiptDataUrl + ')' + 'background-image': + inlineReceiptDataUrl && + imageTransperencyOverlay + 'url(' + '../../../../assets/images/pdf-receipt-placeholder.png' + ')' }" >
@@ -77,7 +80,7 @@ [ngStyle]="{ 'background-image': expense.tx_dataUrls?.length > 0 && - imageTransperencyOverlay + 'url(' + this.expense.tx_dataUrls[0].thumbnail + ')' + imageTransperencyOverlay + 'url(' + '../../../../assets/images/pdf-receipt-placeholder.png' + ')' }" >
@@ -91,7 +94,7 @@ [ngStyle]="{ 'background-image': expense.tx_dataUrls?.length > 0 && - imageTransperencyOverlay + 'url(' + this.expense.tx_dataUrls[0].thumbnail + ')' + imageTransperencyOverlay + 'url(' + '../../../../assets/images/pdf-receipt-placeholder.png' + ')' }" > { let component: ExpensesCardComponent; let fixture: ComponentFixture; @@ -79,8 +65,6 @@ describe('ExpensesCardComponent', () => { ]); const orgUserSettingsServiceSpy = jasmine.createSpyObj('OrgUserSettingsService', ['get']); const fileServiceSpy = jasmine.createSpyObj('FileService', [ - 'getFilesWithThumbnail', - 'downloadThumbnailUrl', 'downloadUrl', 'getReceiptDetails', 'readFile', @@ -89,8 +73,6 @@ describe('ExpensesCardComponent', () => { 'post', ]); - fileServiceSpy.getFilesWithThumbnail.and.returnValue(of(fileObjectData1)); - fileServiceSpy.downloadThumbnailUrl.and.returnValue(of(thumbnailUrlMockData1)); fileServiceSpy.downloadUrl.and.returnValue(of('/assets/images/add-to-list.png')); const popoverControllerSpy = jasmine.createSpyObj('PopoverController', ['create']); const networkServiceSpy = jasmine.createSpyObj('NetworkService', ['connectivityWatcher', 'isOnline']); @@ -174,7 +156,6 @@ describe('ExpensesCardComponent', () => { component.isConnected$ = of(true); component.isSycing$ = of(true); component.isPerDiem = true; - component.receiptThumbnail = 'assets/svg/pdf.svg'; component.isSelectionModeEnabled = false; component.etxnIndex = 1; componentElement = fixture.debugElement; @@ -224,69 +205,6 @@ describe('ExpensesCardComponent', () => { }); describe('getReceipt', () => { - it('should get the receipts when the file ids are present and the length of the thumbnail files array is greater that 0', fakeAsync(() => { - fileService.getFilesWithThumbnail.and.returnValue(of([fileObjectData])); - fileService.downloadThumbnailUrl.and.returnValue(of(thumbnailUrlMockData1)); - component.expense = { - ...expenseData1, - tx_file_ids: ['fiGLwwPtYD8X'], - }; - component.getReceipt(); - fixture.detectChanges(); - tick(500); - expect(component.receiptThumbnail).toEqual(thumbnailUrlMockData1[0].url); - expect(fileService.getFilesWithThumbnail).toHaveBeenCalledOnceWith(component.expense.tx_id); - expect(fileService.downloadThumbnailUrl).toHaveBeenCalledOnceWith('fiHPZUiichAS'); - })); - - it('should get the receipts when the file ids are present and there are no thumbnail files and set the icon to fy-expense when type is not pdf', fakeAsync(() => { - const mockDownloadUrl = { - url: 'mock-url', - }; - fileService.getFilesWithThumbnail.and.returnValue(of([])); - fileService.downloadUrl.and.returnValue(of(mockDownloadUrl.url)); - fileService.getReceiptDetails.and.returnValue('mock-url'); - - component.expense = { - ...expenseData1, - tx_file_ids: ['fiGLwwPtYD8X'], - }; - component.getReceipt(); - fixture.detectChanges(); - tick(500); - expect(fileService.getFilesWithThumbnail).toHaveBeenCalledOnceWith(component.expense.tx_id); - expect(fileService.downloadUrl).toHaveBeenCalledOnceWith('fiGLwwPtYD8X'); - expect(fileService.getReceiptDetails).toHaveBeenCalledOnceWith('mock-url'); - expect(component.receiptIcon).toEqual('assets/svg/fy-expense.svg'); - })); - - it('should get the receipts when the file ids are present and there are no thumbnail files and set the icon to fy-pdf when type is pdf', fakeAsync(() => { - const mockDownloadUrl = { - url: 'mock-url', - }; - - const thumbnailUrlMockRes = { - ...thumbnailUrlMockData1, - url: '/assets/mock-url.pdf', - }; - fileService.getFilesWithThumbnail.and.returnValue(of([])); - fileService.downloadThumbnailUrl.and.returnValue(of(thumbnailUrlMockRes)); - fileService.downloadUrl.and.returnValue(of(mockDownloadUrl.url)); - fileService.getReceiptDetails.and.returnValue('pdf'); - - component.expense = { - ...expenseData1, - tx_file_ids: ['fiGLwwPtYD8Y'], - }; - component.getReceipt(); - fixture.detectChanges(); - tick(500); - expect(fileService.getFilesWithThumbnail).toHaveBeenCalledOnceWith(component.expense.tx_id); - expect(fileService.downloadUrl).toHaveBeenCalledOnceWith('fiGLwwPtYD8Y'); - expect(fileService.getReceiptDetails).toHaveBeenCalledOnceWith('mock-url'); - expect(component.receiptIcon).toEqual('assets/svg/pdf.svg'); - })); - it('should set the receipt icon to fy-mileage when the fyle catergory is mileage', () => { component.expense = { ...expenseData1, @@ -742,7 +660,6 @@ describe('ExpensesCardComponent', () => { fileService.post.and.returnValue(of(fileObjectData)); spyOn(component, 'matchReceiptWithEtxn').and.callThrough(); - spyOn(component, 'setThumbnail').and.callThrough(); component.attachReceipt(receiptDetailsaRes); tick(500); @@ -751,7 +668,6 @@ describe('ExpensesCardComponent', () => { expect(transactionsOutboxService.fileUpload).toHaveBeenCalledOnceWith(dataUrl, attachmentType); expect(component.matchReceiptWithEtxn).toHaveBeenCalledOnceWith(fileObj); expect(fileService.post).toHaveBeenCalledOnceWith(fileObj); - expect(component.setThumbnail).toHaveBeenCalledOnceWith(fileObjectData.id, attachmentType); expect(component.attachmentUploadInProgress).toBeFalse(); tick(500); })); @@ -896,32 +812,6 @@ describe('ExpensesCardComponent', () => { })); }); - describe('setThumbnail():', () => { - it('should set the thumbnail', fakeAsync(() => { - const fileObjid = fileObjectData.id; - const attachmentType = 'pdf'; - fileService.downloadUrl.and.returnValue(of('mock-url')); - component.setThumbnail(fileObjid, attachmentType); - fixture.detectChanges(); - tick(500); - expect(component.receiptIcon).toEqual('assets/svg/pdf.svg'); - expect(fileService.downloadUrl).toHaveBeenCalledOnceWith(fileObjid); - })); - - it('should set the receipt thumbnail to download url when the attatchment tyoe is not pdf', fakeAsync(() => { - component.receiptIcon = undefined; - const fileObjid = fileObjectData.id; - const attachmentType = 'png'; - fileService.downloadUrl.and.returnValue(of('/assets/images/add-to-list.png')); - component.setThumbnail(fileObjid, attachmentType); - fixture.detectChanges(); - tick(500); - expect(component.receiptThumbnail).toEqual(thumbnailUrlMockData1[0].url); - expect(component.receiptIcon).toBeUndefined(); - expect(fileService.downloadUrl).toHaveBeenCalledOnceWith(fileObjid); - })); - }); - it('setupNetworkWatcher(): should setup the network watcher', fakeAsync(() => { networkService.isOnline.and.returnValue(of(true)); const eventEmitterMock = new EventEmitter(); diff --git a/src/app/shared/components/expenses-card/expenses-card.component.ts b/src/app/shared/components/expenses-card/expenses-card.component.ts index 64789dd34e..141c41bebc 100644 --- a/src/app/shared/components/expenses-card/expenses-card.component.ts +++ b/src/app/shared/components/expenses-card/expenses-card.component.ts @@ -101,7 +101,7 @@ export class ExpensesCardComponent implements OnInit { attachmentUploadInProgress = false; - receiptThumbnail: string = null; + isReceiptPresent: boolean; isConnected$: Observable; @@ -135,7 +135,7 @@ export class ExpensesCardComponent implements OnInit { private trackingService: TrackingService, private currencyService: CurrencyService, private expenseFieldsService: ExpenseFieldsService, - private orgSettingsService: OrgSettingsService, + private orgSettingsService: OrgSettingsService ) {} get isSelected(): boolean { @@ -167,36 +167,7 @@ export class ExpensesCardComponent implements OnInit { this.receiptIcon = 'assets/svg/fy-expense.svg'; } } else { - this.fileService - .getFilesWithThumbnail(this.expense.tx_id) - .pipe( - map((ThumbFiles: FileObject[]) => { - if (ThumbFiles.length > 0) { - this.fileService - .downloadThumbnailUrl(ThumbFiles[0].id) - .pipe( - map((downloadUrl: FileObject[]) => { - this.receiptThumbnail = downloadUrl[0].url; - }), - ) - .subscribe(noop); - } else { - this.fileService - .downloadUrl(this.expense.tx_file_ids[0]) - .pipe( - map((downloadUrl: string) => { - if (this.fileService.getReceiptDetails(downloadUrl) === 'pdf') { - this.receiptIcon = 'assets/svg/pdf.svg'; - } else { - this.receiptIcon = 'assets/svg/fy-expense.svg'; - } - }), - ) - .subscribe(noop); - } - }), - ) - .subscribe(noop); + this.isReceiptPresent = true; } } } @@ -281,7 +252,7 @@ export class ExpensesCardComponent implements OnInit { const orgSettings$ = this.orgSettingsService.get().pipe(shareReplay(1)); this.isSycing$ = this.isConnected$.pipe( - map((isConnected) => isConnected && this.transactionOutboxService.isSyncInProgress() && this.isOutboxExpense), + map((isConnected) => isConnected && this.transactionOutboxService.isSyncInProgress() && this.isOutboxExpense) ); this.isMileageExpense = this.expense.tx_org_category && this.expense.tx_org_category?.toLowerCase() === 'mileage'; @@ -301,13 +272,13 @@ export class ExpensesCardComponent implements OnInit { .pipe( map((homeCurrency) => { this.homeCurrency = homeCurrency; - }), + }) ) .subscribe(noop); this.isProjectEnabled$ = orgSettings$.pipe( map((orgSettings) => orgSettings.projects && orgSettings.projects.allowed && orgSettings.projects.enabled), - shareReplay(1), + shareReplay(1) ); if (!this.expense.tx_id) { @@ -315,7 +286,7 @@ export class ExpensesCardComponent implements OnInit { } else if (this.previousExpenseTxnDate || this.previousExpenseCreatedAt) { const currentDate = this.expense && new Date(this.expense.tx_txn_dt || this.expense.tx_created_at).toDateString(); const previousDate = new Date( - (this.previousExpenseTxnDate || this.previousExpenseCreatedAt) as string, + (this.previousExpenseTxnDate || this.previousExpenseCreatedAt) as string ).toDateString(); this.showDt = currentDate !== previousDate; } @@ -453,16 +424,6 @@ export class ExpensesCardComponent implements OnInit { } } - setThumbnail(fileObjId: string, attachmentType: string): void { - this.fileService.downloadUrl(fileObjId).subscribe((downloadUrl) => { - if (attachmentType === 'pdf') { - this.receiptIcon = 'assets/svg/pdf.svg'; - } else { - this.receiptThumbnail = downloadUrl; - } - }); - } - matchReceiptWithEtxn(fileObj: FileObject): void { this.expense.tx_file_ids = []; this.expense.tx_file_ids.push(fileObj.id); @@ -482,17 +443,17 @@ export class ExpensesCardComponent implements OnInit { }), finalize(() => { this.attachmentUploadInProgress = false; - }), + }) ) - .subscribe((fileObj: FileObject) => { - this.setThumbnail(fileObj.id, attachmentType); + .subscribe(() => { + this.isReceiptPresent = true; }); } setupNetworkWatcher(): void { const networkWatcherEmitter = this.networkService.connectivityWatcher(new EventEmitter()); this.isConnected$ = concat(this.networkService.isOnline(), networkWatcherEmitter.asObservable()).pipe( - startWith(true), + startWith(true) ); } diff --git a/src/assets/images/pdf-receipt-placeholder.png b/src/assets/images/pdf-receipt-placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..619061247bb2fcd87058fce92722a83328bd839f GIT binary patch literal 7933 zcmcgx)n6M-unol$3ME)65FkKscefDS-5pxAIK_)wad+1iYm1cPQlz+Rad&sQe82k# z+_(F%yR-AM`!I9P%$x`nC21@SVhjKPfF&z~P0U{!_ih%q9rmO?!W94ku2e#>BCDOK6)Q}(@Ns{%O=uf*V!-BtI{uBGdTexDa*^9 zr6x+4)z#Fb20lj>BtVmx^|iigR2_sTpg)}Uk|)r?qUfmQlHzJPEW0WABJt@dBjBCk zuY?E~-gLwzCnt|CGipL0uiA0`P=Fz-kXIf58J*Jj#zN6Ssp;wIE-qTrBF{^8wy6hm zKc(AT>?#ub1fJga`_;4F&(U5vT*WSZY2at%v^~D>yCBW2yY{6P#3jCj09zlA^!d*R zIg33`*M3-yQF=Z|{({MjtVpcCzO4h2>>rXZIqyIw4 znsRr!KM+D_M9124kzn95#I06sBrKd_U)^$*k>$||`f+o;ym``vok|rer1pL|i{5s+ zB7SQqb08dts>phxM6Jo)KApodQlmmeRlxNCdfakdHJHJz|2LVDme9~O-sk?XVZ2Bd zFP+yR$6)m7)vZ}K;n@IdTOUOjg!0ppXK4~h*9Pi}(EV-}BC;tIPwsmW??Ggw`^zyO z@@dgYE^b*Eslj$eNh+3%xtGkP!s5N<-lt#$nb~?zwA!5f1;vJW48N4f)6F`aDv#Z4 z-m9#AQ0j3-wN_<@&q)XRc&o2B_0D*){N~G}sS6HmhR5c&XPe_!i}f6zj)bTuc1v?v zmVM5nA1-H%&!+Vq`eR51i!{pB&aRg~54`xQ?_IDa;HKhbzDaKT)4O>hS_(ElOqBJ&&R8WgU>jQ#&CWTqwVtY7y$*~*3%@0Ax3Bx2Rm=ARkX9)jc z(T_X#+wEIXaq%Xbr73)eOD>r~Q{!D2se2li?X<#B-(sUnVe6}sg2E3Np;-3{>Pr<4 zOBIb8m3o>s<&9|-#3g=@XwFlb+U5KlCSL{I59rz+dE{g7*S@f76pA6j)md7yFf#Y^ z>V8e1tafeQZs*P%T5%smxTP96M=0b7>Gm&{tJ6+dQOl=bX7{thU{k$$dDlU)YrDgY zgJYAlf&&dKjGTA1yNs#=8(d%1o?6$S=5 z(sqMa-JY{YNEfo9e#o9A<5O>hTN?fms_}>FTCd2ROTZKGS|uNjD%&roN*;P_iWtYU zJYAFI@HnxxSzs4T3se?;)W{xER~FU`QFaY92B?A|Tfv(Vv^izlD}sIl^_2@;HoO0t z8l)6&c(VvA;~Qz!>+Sv(lyty9`ozqHHCM&QBjK(T)mx3h88p8ZwSs5{YEgQ1-Yq`2 zK8@#@B3sYqzLr%b57~l+&m6&z1dx zCe=UhV_`C+xBV}fP`P?hqL^7A%X@?sUFR~R#J?WJDfClawYte>iUCBDkV6N2$xuBG z^Bzq8>qVE%0CcjrH-rXI(Wje2jjsl>ia5%?nLPHcpJkk0kk!`W{46h(TNh1ykIY#6 z{gZ%t*OqN{i&h;j@#aop?6l)A+nFi`4=((Sa-A`=456T`4coqvIP#83kKtbx`fWAUof?1{0dQX3Q0J4O(S^Px_I>GYK4D*^#typSOV8Cxz!^kIjS7&d}w+526jmkxaepn7@ zs6RZMk2p{3ekk|XRi?xWAQ`2MCgDw?&GCSPK)U@m1=M}xfxOE;>MTtM9O*pv8Af5Y z&DTrbavTt=@JMBZM^w-4-G23yP@~Y_Ho$YN%_@U33?`nLcSNg@28|}>Rx_=MZjO+Y z0;AcBdy&`0o{WL*q~qd{+k&WRIrvl&`@L3J#8Si2!v?LPuXv z>f6sRaqQG-BtA*}E?Z?O@TQw;>EIysNY1F+Q^BD{9ERs58w9-{swG~45)osfe{$=wp{|Z*{4RPe zZiOAM^TAx$Q@jB0qpSq}XPSwXk+M6^qI!gkq}mr5xjoUjd8?8nLpA4|t~L>)Q0;SC zdK{O+#^eK^NK0)Bn%lRp8xC7%M^e7end%c}whZmF=2{iNqIXxzY4K@%Hjry>g#z>j zR((A<0?K`2OpU*hBGDMshO`|~F_a~JMtcUg8ZlCP`__k!$4yT_dE|z^`)uE3@p4mZ zK40On8QG9KR(i}W)KFMw&Baqmf6bs7YVka$*_}Clw!Hb!RlGIE&0X8v{yO5rP)2b{ zVBgvc8ELhls0ZuyR`2+^ee%5cF1@*x>7XTwW*kt%%hL(*eRs_;g5b89)R8c%4`KG=SAj)KQAl^HN3N&srQ`M3Zh7Ot6MGbk zUTT^>f!J>o{EH~$x_getD7n8TNeOHUi|1PEr z8&I@Z$WJh$vEpy!wOi~JeSg%%TD$oQJ+fQy=Umqrtit_pk@`}pY+HC&-g2$mnCmx` zAUFPd56L-wX*|}1ingaJcDfcvUR<9XS%gu=BH$3Q5synWJcOh;<*h^?qR2yM<%iM= zqH4WiasT*y`|!rD+2vWEZ)X4#3KD%aA6RqNblf^Y9$eZwk2A&PE$4?aq0(`0cMVxG z680|l2l^^n`JnS%CO%~0iL-OufkwVhdx^JX7`e$GtY6be*fT1GCz-CDSPx1 zKC{trh-xCPJPpo z-{-01_OTp&L$P+UrhrT40@1XKqT%Eax(LcARx|hFBa=>Lr|=;yC}uJIjp&y`!ZjDP z@KSbg{*9X$9*P^PZS1S*^=#}gRLKXEoeVmmNAw)96+L&pX0_Uh6x*Nv#`pqL)iDdR ziNk4Fo|sah5juP&5wuppLVledm1aS(s8jGijcOyqSc+vcnD~Sv7TRsvQ2@QEpq~9% zQ9p60au$~=gi?l26)e)voC3@~N%X=Q#B!rdIfqKv(uA>e42P@`&a#eS&+aQrhU_aPM)-#=)C&C}4YGnq46#3UGc7(6w0P*H7=uztJnSxKUzK0=$vWTrL) z@7ev?^I_qj)!w6hi<*o?dXpM@o-wl!zTa4R!1pJz?%EsX-Uf4HDEzLn1m z>8k?^1Ww!9$0|&%=o#P*Nu6G{RuMQvh!Py@F;ReL)}F(*ObsdH*$Pgm!0}K!1@zB9 z5rAX{t?{x@YEhUeqkI98NPL7|e1Mpr2})nA^+KAB>Yp!K9t^sBjSQNREORqee-bX4 z`kTNpw&jJTvQY7ewWZrgX*7_1P275;@iTgdGqVDXBg?EUj@-LFsg zq87#jQswNuj9OK32ntiQ@vV*d1cpzM(JJ2uvEKV0B#;xV{@CdEe(8ehfc>2&^*A=E zOav5IFbm-#v(H>eIPpeZBqlHmQcf?|4dSKcm#(92cp(1+M)qqya*$9Hjl2jjw%Bl@ zt!OX+My~p1-k&<2Bsd6C-S+hReSH7J+M;vp!}G&TxZ&Qlvn6%BOa{J)qK(3ixQXOc z(dn}9wV`4Y9<^ytnQNwi{nsq_d279kZvOPoUA0&=7N!g;k5))<+a{Wf0_|!e=LP!) z>mSj_5@ByNCrn~Eh%AQ1)w4XVyTe?ZF*#8k4eFXXTw)UfFE`wDq?1WEB!ggr34b?- zp-4QrAieXg5p`zEVV3qDDytl?9Otc>GsYA($0p&Q;%eAtV!&E*;t;kRBZef6yg!3T%I+^j)^V|M zDJ=nnOtg4^A~-XT6)s8FoIZ!8GNhC0d5Zx2UMUN7i|HsRhL}WYaU#b#W}%R6z(?Z% zB61`nWg zg8=0_6xQ>IlC(Axzara!Z6P&w>Fmm(DRKWBqBI;EW7$bV@1Z&Y;ft5Dawdx9Uaz|Y zT@hChRYKHhK1i>Pz=woee91SDTD#Jq*uuiXXW`2JtcS$Gsu5+w{297?Zy`3mxY;TB z+9e7109&h9z-bN9csZ?K2rqf0eVkB{v;3Q)I=<4(QKM7rOi-ywab{Y;cl%bp^R!6a z*}o?*TWUayx$t^jxji14A}Vw;3muLW4yQ@?k+Tp}a}~8Qfl_S^qH$1wp9uW$_q$$- zfJm&{ZPemswzP=t^X-ut+LTByKOZUIv3F5uh0~82NUP=<@6h2U_}08Oi!zFeuH>v- zQe7*vW}!^#k-8F$PFO%OY2=uID2+87U-tQ^=bxVEZ)3A)%hd-Z01+qpqk&Oc>M};n z2&c35)8?)wegEb>(OhdNUpTZAx9t?0}0eYMz_3x3J zWIzEfA(c_MoS{2YvFB>3ulUo_hTVt5)D^t33J42mhhGyp>3~KkXy=3X)$t_> zNUwXI#LoG1c)lF|&K{KcCH39r>U5oUXX2e;OajSGJPtTsX@kwERLMzl~_iI$!F3#~U_cG%9TJtSD7p+rGm~ zVJM8;uWY%J0=nIfZAx1F%Bo6>S>Iaqn#7?spC*nDojJI%0~Vii?Bn#I^7)U$o&B`K zJa>SK(Fc-XfGp1C$+|i~(aF`Q9nYvvPScm;l;2SbB*9O(4NK*i#N$dF*m3Qih%SLD z(D?A6Czqi!!_Q)_2(7TD6T2nw@+c;SinlQP3w+I~IlM4EPolvhFe=&JPql_U8JM!H zvh=~QxJP^KOOl~aLiPV5QKQ{=m={cnNoZL7QWt#-Aq{)~vN$8Bvt2>scx zokcGlM0guvkV=pb*?(-;^)$gK2^)Qc5-a0o?Nlt?wW zN>?-1dol(JmFaX!gX)=$e!wzyA`9&QFp#Jz)S(}+~T#WKmMsIZY&!lCt0)Omkgn4g%S?I&F+uA>L zB}T+RoSdFY!oyArQ9x^4!$>ajQ{F~y!pCWJO!lZjhu{!bQ9x8OYFc9pbBQs|feVguV*{K>tV02D0cR+GDd{Eih6F4zu!S$; z-b$#lZxf1e(_CM-8Gqa4LxYRkMgBPB7=X@!i3^~7zjYpf3-?<4yx~>!VV??EHSL?@0F_x2`<~hl1V#*1 zq=7uXOMF9{v0a7K0IO^Q63glq7?=-}Yj)qbUub|%K0mGSM)>hXpOI5)tvvixux|E! zB)I#15%aymUVJGwn?y&$oHgu+Z$Z3{mPcf{ISvE4=?4KQ(65sn*m1lSOmud^*J>ga&Y zg@8W{X5~LFq*c6$I7^(Ey||`1bZ)=wk99N@pw}VSZ@<*-g>lk!OE2d`)PiYOw?HXx z0GuTwqm3=DSkMrqUOJcQEyISE@CQGc1GvQYjzM)^cE)bLR-F?eAqbeucDZ85bq zZ4WS%Jy3{(f+8MLNWVArt<)uW*E+Drgp~PbMKQnV*C9cre!qTh8O3M?V)R?tOKBNx zC2~$5%0VpBAG^0#H|w$c;XF_HiE9*WdYJ02U0A#*Iq(cSVDzqO@Joep1HpV#FnkKy zA<{3s67@B)Z`4l0r^@q*ZL(lQ14ilQ7viDRyV2&lCcMFbf2@rda(ZJsiT70~^3qb$ zd$a^XdsSYVGyXkkQNyd2*AD4*UwD{xhiK;{n@4z|0;p3dmMIP6i0E9JbB&FVHRSJw z$w#2%E@%J>sZQn;4XXMA#Qcn6x~jn8^<}Gl&Qc5~fB^qQ)X@kuK!K^m$jree5%uwS zsZPIPLLllBt+ZEF@Oqu&=}StRte5k3IU!EKsG`AMV42gGfvNS3G+e8ITtRbz$9{7l z-uNr{^^a)2nbJ2UpN^O2IX|wn2V^vLz~B&|_3?GSA$>_iGnT_XkP)7oz6shyryUpj zsnXn?DEM)aFWObc@4wMn6HJ`Ws}auTeX1tj0e7ovbL@a0N`3*YE%}FRGL?TUgarC1 z>pbFtiKERb+n=xC`aiyvdFHH$S7S!DLXdI+Q^wmj0&Y2EvgNP`&~_lt8NgKLegm@%L%#p=Tc!S6JOzw6Qa|@!T!=p}C}s|8C*!kMiPt zS~)w;6_25Cd(rz@zdHe5sxS-58C^#2;oZ+p@aAdbX~M$v z*qzP4JGc3xKwj6eVs>-I(E5Rz6rBqu-6r=+jjuYg^&c0AJ*iS@9gGBfzcX1UbTtL6 zytA*f!jY>csFL6$Gr~$uF0UO=dAmR3Im4u<5nKF-I+NE1Vea1fqNbxl_nT-`B1KEL zSp#G`{yynSC)ej8q9|E*!mpMKt*G?xi}qG~9^W$cTJ1%DQVhi;qBLxBPhRZ``xdwR z4Ngey)#D8LS1RbbllNHy$c2UF>QPM*y!KD%WxPL8mDv0@CMEwuCcq*DDWp?+ZlBxf z{aZElD-Oka;jrD4^dv|x`pKt*7Ln6o!!)p0!K{AW8i+e*bPY82@Wo?4=!WQNA0Ph0 z&U+_(ytw$&@A+;!#ATCo@bIvofIazfIMhbNR5{ByBn%1h zOhDhsAb%@7FL(>ye<~Ys4sPf8G56sp7rj=qjz%5=ilCmZjNb|Mt3mMoYx*=(6^~#G zo@D}g&c$jK-!KL@x#lizN(!q4xO~T+*?{A=bh-uaV))1RM>(Kh*?R*V(ac zkH4(Z3y55-bbF+D>m9flrq|S`JC*JtJWkyqksfm3euiAbYE<|8H8~UR4l1x|6_=HI~ e!p04rv6247CCn5NZNI4109i>TL>1g5@P7cStNyb9 literal 0 HcmV?d00001 From 57802db11b72987ac238918cf47cdcf644f5b1ce Mon Sep 17 00:00:00 2001 From: Suyash Patil <127177049+suyashpatil78@users.noreply.github.com> Date: Mon, 9 Oct 2023 19:38:24 +0530 Subject: [PATCH 10/14] test: 38% coverage of my-expenses service (#2486) * test: my-expenses service test * major * test: 75% coverage of my-expense service (#2489) * test: 75% coverage of my-expense service * test: 100% coverage of my-expenses service (#2490) * test: 100% coverage of my-expenses service * minor --- .../core/mock-data/expense-filters.data.ts | 31 ++ src/app/core/mock-data/filter-options.data.ts | 128 +++++ src/app/core/mock-data/filter-pills.data.ts | 46 ++ .../core/mock-data/selected-filters.data.ts | 84 +++ .../my-expenses/my-expenses.service.spec.ts | 520 ++++++++++++++++++ .../fyle/my-expenses/my-expenses.service.ts | 40 +- 6 files changed, 831 insertions(+), 18 deletions(-) create mode 100644 src/app/fyle/my-expenses/my-expenses.service.spec.ts diff --git a/src/app/core/mock-data/expense-filters.data.ts b/src/app/core/mock-data/expense-filters.data.ts index fb97d91495..3262dacc42 100644 --- a/src/app/core/mock-data/expense-filters.data.ts +++ b/src/app/core/mock-data/expense-filters.data.ts @@ -19,3 +19,34 @@ export const expenseFiltersData2: Partial = { sortDir: 'asc', splitExpense: 'YES', }; + +export const expenseFiltersData3: Partial = { + state: 'custom', + date: 'Last Month', + customDateStart: new Date('2023-01-04'), + customDateEnd: new Date('2023-01-10'), + receiptsAttached: 'Yes', + type: ['Mileage'], + cardNumbers: ['1234', '2389'], + splitExpense: 'Yes', +}; + +export const expenseFiltersData4: Partial = { + ...expenseFiltersData3, + customDateStart: undefined, + customDateEnd: undefined, +}; + +export const expenseFiltersData5: Partial = { + receiptsAttached: 'YES', + sortParam: 'tx_amount', + sortDir: 'desc', + splitExpense: 'YES', +}; + +export const expenseFiltersData6: Partial = { + receiptsAttached: 'YES', + sortParam: 'tx_txn_dt', + sortDir: 'asc', + splitExpense: 'YES', +}; diff --git a/src/app/core/mock-data/filter-options.data.ts b/src/app/core/mock-data/filter-options.data.ts index 67edd81e8b..b6619c378f 100644 --- a/src/app/core/mock-data/filter-options.data.ts +++ b/src/app/core/mock-data/filter-options.data.ts @@ -2,6 +2,7 @@ import { FilterOptionType } from 'src/app/shared/components/fy-filters/filter-op import { FilterOptions } from 'src/app/shared/components/fy-filters/filter-options.interface'; import { AdvancesStates } from '../models/advances-states.model'; import { SortingValue } from '../models/sorting-value.model'; +import { DateFilters } from 'src/app/shared/components/fy-filters/date-filters.enum'; export const filterOptions: FilterOptions[] = [ { @@ -50,3 +51,130 @@ export const filterOptions: FilterOptions[] = [ ], }, ]; + +export const filterOptions2: FilterOptions[] = [ + { + name: 'Type', + optionType: FilterOptionType.multiselect, + options: [ + { + label: 'Complete', + value: 'READY_TO_REPORT', + }, + { + label: 'Policy Violated', + value: 'POLICY_VIOLATED', + }, + { + label: 'Cannot Report', + value: 'CANNOT_REPORT', + }, + { + label: 'Incomplete', + value: 'DRAFT', + }, + ], + }, + { + name: 'Date', + optionType: FilterOptionType.date, + options: [ + { + label: 'All', + value: DateFilters.all, + }, + { + label: 'This Week', + value: DateFilters.thisWeek, + }, + { + label: 'This Month', + value: DateFilters.thisMonth, + }, + { + label: 'Last Month', + value: DateFilters.lastMonth, + }, + { + label: 'Custom', + value: DateFilters.custom, + }, + ], + }, + { + name: 'Receipts Attached', + optionType: FilterOptionType.singleselect, + options: [ + { + label: 'Yes', + value: 'YES', + }, + { + label: 'No', + value: 'NO', + }, + ], + }, + { + name: 'Expense Type', + optionType: FilterOptionType.multiselect, + options: [ + { + label: 'Mileage', + value: 'Mileage', + }, + { + label: 'Per Diem', + value: 'PerDiem', + }, + { + label: 'Regular Expenses', + value: 'RegularExpenses', + }, + ], + }, + { + name: 'Sort By', + optionType: FilterOptionType.singleselect, + options: [ + { + label: 'Date - New to Old', + value: 'dateNewToOld', + }, + { + label: 'Date - Old to New', + value: 'dateOldToNew', + }, + { + label: 'Amount - High to Low', + value: 'amountHighToLow', + }, + { + label: 'Amount - Low to High', + value: 'amountLowToHigh', + }, + { + label: 'Category - A to Z', + value: 'categoryAToZ', + }, + { + label: 'Category - Z to A', + value: 'categoryZToA', + }, + ], + }, + { + name: 'Split Expense', + optionType: FilterOptionType.singleselect, + options: [ + { + label: 'Yes', + value: 'YES', + }, + { + label: 'No', + value: 'NO', + }, + ], + }, +]; diff --git a/src/app/core/mock-data/filter-pills.data.ts b/src/app/core/mock-data/filter-pills.data.ts index a2067930fb..0c8bb1c1ea 100644 --- a/src/app/core/mock-data/filter-pills.data.ts +++ b/src/app/core/mock-data/filter-pills.data.ts @@ -154,3 +154,49 @@ export const filterTypeMappings: FilterPill[] = [ sortFilterPill, splitExpenseFilterPill, ]; + +export const sortByDescFilterPill: FilterPill[] = [ + { + label: 'Sort By', + type: 'sort', + value: 'amount - high to low', + }, +]; + +export const sortByAscFilterPill: FilterPill[] = [ + { + label: 'Sort By', + type: 'sort', + value: 'amount - low to high', + }, +]; + +export const sortByDateAscFilterPill: FilterPill[] = [ + { + label: 'Sort By', + type: 'sort', + value: 'date - old to new', + }, +]; + +export const sortByDateDescFilterPill: FilterPill[] = [ + { + label: 'Sort By', + type: 'sort', + value: 'date - new to old', + }, +]; + +export const expectedDateFilterPill = [ + { + label: 'Date', + type: 'date', + value: '2023-01-21 to 2023-01-31', + }, +]; + +export const stateFilterPill2: FilterPill = { + label: 'Type', + type: 'state', + value: 'Incomplete, Complete, approved', +}; diff --git a/src/app/core/mock-data/selected-filters.data.ts b/src/app/core/mock-data/selected-filters.data.ts index 4b57278569..26f5531513 100644 --- a/src/app/core/mock-data/selected-filters.data.ts +++ b/src/app/core/mock-data/selected-filters.data.ts @@ -132,3 +132,87 @@ export const selectedFiltersParams2: SelectedFilters[] = [ }, }, ]; + +export const selectedFilters7: SelectedFilters[] = [ + ...selectedFilters5, + { + name: 'Type', + value: 'custom', + }, + { + name: 'Receipts Attached', + value: 'Yes', + }, + { + name: 'Expense Type', + value: ['Mileage'], + }, + { + name: 'Cards', + value: ['1234', '2389'], + }, + { name: 'Sort By', value: 'dateNewToOld' }, + { + name: 'Split Expense', + value: 'Yes', + }, +]; + +export const selectedFilters8: SelectedFilters[] = [ + { + name: 'Date', + value: 'Last Month', + }, + { + name: 'Type', + value: 'custom', + }, + { + name: 'Receipts Attached', + value: 'Yes', + }, + { + name: 'Expense Type', + value: ['Mileage'], + }, + { + name: 'Cards', + value: ['1234', '2389'], + }, + { name: 'Sort By', value: 'dateNewToOld' }, + { + name: 'Split Expense', + value: 'Yes', + }, +]; + +export const selectedFilters9: SelectedFilters[] = [ + { + name: 'Type', + value: ['DRAFT', 'READY_TO_REPORT'], + }, + { + name: 'Receipts Attached', + value: 'YES', + }, + { + name: 'Date', + value: 'thisWeek', + associatedData: { + startDate: undefined, + endDate: undefined, + }, + }, + { + name: 'Expense Type', + value: ['PerDiem', 'Mileage'], + }, + { + name: 'Cards', + value: ['1234', '5678'], + }, + { + name: 'Split Expense', + value: 'YES', + }, +]; diff --git a/src/app/fyle/my-expenses/my-expenses.service.spec.ts b/src/app/fyle/my-expenses/my-expenses.service.spec.ts new file mode 100644 index 0000000000..c64042e787 --- /dev/null +++ b/src/app/fyle/my-expenses/my-expenses.service.spec.ts @@ -0,0 +1,520 @@ +import { TestBed } from '@angular/core/testing'; +import { MyExpensesService } from './my-expenses.service'; +import { + expenseFiltersData1, + expenseFiltersData3, + expenseFiltersData4, + expenseFiltersData5, + expenseFiltersData6, +} from 'src/app/core/mock-data/expense-filters.data'; +import { + cardFilterPill, + creditTxnFilterPill, + expectedDateFilterPill, + expectedFilterPill2, + receiptsAttachedFilterPill, + sortByAscFilterPill, + sortByDateAscFilterPill, + sortByDateDescFilterPill, + sortByDescFilterPill, + sortFilterPill, + splitExpenseFilterPill, + stateFilterPill2, +} from 'src/app/core/mock-data/filter-pills.data'; +import { selectedFilters7, selectedFilters8, selectedFilters9 } from 'src/app/core/mock-data/selected-filters.data'; +import { FilterPill } from 'src/app/shared/components/fy-filter-pills/filter-pill.interface'; +import { DateFilters } from 'src/app/shared/components/fy-filters/date-filters.enum'; +import { + expectedFilterPill3, + expectedFilterPill4, + expectedFilterPill5, + expectedFilterPill6, + expectedFilterPill7, + expectedFilterPill8, + expectedFilterPill9, +} from 'src/app/core/mock-data/my-reports-filterpills.data'; +import { filter1, filter2 } from 'src/app/core/mock-data/my-reports-filters.data'; +import { filterOptions2 } from 'src/app/core/mock-data/filter-options.data'; + +describe('MyExpensesService', () => { + let myExpensesService: MyExpensesService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [MyExpensesService], + }); + + myExpensesService = TestBed.inject(MyExpensesService); + }); + + it('should be created', () => { + expect(myExpensesService).toBeTruthy(); + }); + + it('generateSortFilterPills(): should call generateSortTxnDatePills, generateSortAmountPills and generateSortCategoryPills once', () => { + spyOn(myExpensesService, 'generateSortTxnDatePills'); + spyOn(myExpensesService, 'generateSortAmountPills'); + //@ts-ignore + spyOn(myExpensesService, 'generateSortCategoryPills'); + + myExpensesService.generateSortFilterPills(expenseFiltersData1, creditTxnFilterPill); + + expect(myExpensesService.generateSortTxnDatePills).toHaveBeenCalledTimes(1); + expect(myExpensesService.generateSortAmountPills).toHaveBeenCalledTimes(1); + //@ts-ignore + expect(myExpensesService.generateSortCategoryPills).toHaveBeenCalledTimes(1); + }); + + describe('covertFilters():', () => { + it('should modify the selected filters and return the generated filter', () => { + spyOn(myExpensesService, 'convertSelectedSortFitlersToFilters'); + const sortBy = { name: 'Sort By', value: 'dateNewToOld' }; + + const convertedFilters = myExpensesService.convertFilters(selectedFilters7); + + expect(myExpensesService.convertSelectedSortFitlersToFilters).toHaveBeenCalledOnceWith( + sortBy, + expenseFiltersData3 + ); + + expect(convertedFilters).toEqual(expenseFiltersData3); + }); + + it('should set customDateStart and customDateEnd as undefined if associated data is undefined', () => { + spyOn(myExpensesService, 'convertSelectedSortFitlersToFilters'); + const sortBy = { name: 'Sort By', value: 'dateNewToOld' }; + + const convertedFilters = myExpensesService.convertFilters(selectedFilters8); + + expect(myExpensesService.convertSelectedSortFitlersToFilters).toHaveBeenCalledOnceWith( + sortBy, + expenseFiltersData4 + ); + + expect(convertedFilters).toEqual(expenseFiltersData4); + }); + }); + + describe('generateSortAmountPills():', () => { + it('should add amount - high to low as sort params if sort direction is decreasing', () => { + const filterPill = []; + myExpensesService.generateSortAmountPills(expenseFiltersData5, filterPill); + expect(filterPill).toEqual(sortByDescFilterPill); + }); + + it('should add amount - low to high as sort params if sort direction is ascending', () => { + const filterPill = []; + myExpensesService.generateSortAmountPills({ ...expenseFiltersData5, sortDir: 'asc' }, filterPill); + expect(filterPill).toEqual(sortByAscFilterPill); + }); + }); + + describe('generateSortTxnDatePills():', () => { + it('should add date - old to new as sort params if sort direction is ascending', () => { + const filterPill = []; + myExpensesService.generateSortTxnDatePills(expenseFiltersData6, filterPill); + expect(filterPill).toEqual(sortByDateAscFilterPill); + }); + + it('should add date - new to old as sort params if sort direction is descending', () => { + const filterPill = []; + myExpensesService.generateSortTxnDatePills({ ...expenseFiltersData6, sortDir: 'desc' }, filterPill); + expect(filterPill).toEqual(sortByDateDescFilterPill); + }); + }); + + it('generateTypeFilterPills(): should add combined expense types value in filter pills', () => { + const filterPill = []; + myExpensesService.generateTypeFilterPills( + { ...expenseFiltersData1, type: ['RegularExpenses', 'PerDiem', 'Mileage', 'custom'] }, + filterPill + ); + expect(filterPill).toEqual([ + { label: 'Expense Type', type: 'type', value: 'Regular Expenses, Per Diem, Mileage, custom' }, + ]); + }); + + describe('generateDateFilterPills(): ', () => { + it('should generate filter pill for "this Week"', () => { + const filter = { + date: DateFilters.thisWeek, + }; + + const res = myExpensesService.generateDateFilterPills(filter, []); + + expect(res).toEqual(expectedFilterPill5); + }); + + it('should generate filter pill for "this Month"', () => { + const filter = { + date: DateFilters.thisMonth, + }; + + const res = myExpensesService.generateDateFilterPills(filter, []); + + expect(res).toEqual(expectedFilterPill6); + }); + + it('should generate filter pill for "All"', () => { + const filter = { + date: DateFilters.all, + }; + + const res = myExpensesService.generateDateFilterPills(filter, []); + + expect(res).toEqual(expectedFilterPill7); + }); + + it('should generate filter pill for "Last Month"', () => { + const filter = { + date: DateFilters.lastMonth, + }; + + const res = myExpensesService.generateDateFilterPills(filter, []); + + expect(res).toEqual(expectedFilterPill8); + }); + + it('should generate custom date filter pill', () => { + const filter = filter2; + spyOn(myExpensesService, 'generateCustomDatePill').and.returnValue(expectedFilterPill9); + + const res = myExpensesService.generateDateFilterPills(filter, []); + + expect(myExpensesService.generateCustomDatePill).toHaveBeenCalledOnceWith(filter, []); + + expect(res).toEqual(expectedFilterPill9); + }); + }); + + describe('generateCustomDatePill(): ', () => { + it('should generate custom date filter pill with start and end date', () => { + const filter = { + customDateStart: new Date('2023-01-21'), + customDateEnd: new Date('2023-01-31'), + }; + + const res = myExpensesService.generateCustomDatePill(filter, []); + + expect(res).toEqual(expectedDateFilterPill); + }); + + it('should generate custom date filter pill with only start date', () => { + const filter = { + customDateStart: new Date('2023-01-21'), + customDateEnd: null, + }; + + const res = myExpensesService.generateCustomDatePill(filter, []); + + expect(res).toEqual(expectedFilterPill3); + }); + + it('should generate custom date filter pill with only end date', () => { + const filter = { + customDateStart: null, + customDateEnd: new Date('2023-01-31'), + }; + + const res = myExpensesService.generateCustomDatePill(filter, []); + + expect(res).toEqual(expectedFilterPill4); + }); + + it('should not generate custom date filter pill if start and end date are null', () => { + const filter = { + customDateStart: null, + customDateEnd: null, + }; + + const res = myExpensesService.generateCustomDatePill(filter, []); + + expect(res).toEqual([]); + }); + }); + + it('generateReceiptsAttachedFilterPills(): should add receipt attached filter pill', () => { + const filterPill = []; + myExpensesService.generateReceiptsAttachedFilterPills(filterPill, expenseFiltersData1); + expect(filterPill).toEqual([receiptsAttachedFilterPill]); + }); + + it('generateSplitExpenseFilterPills(): should add split expense filter pill', () => { + const filterPill = []; + myExpensesService.generateSplitExpenseFilterPills(filterPill, expenseFiltersData1); + expect(filterPill).toEqual([splitExpenseFilterPill]); + }); + + it('generateCardFilterPills(): should add card filter pill', () => { + const filterPill = []; + myExpensesService.generateCardFilterPills(filterPill, expenseFiltersData1); + expect(filterPill).toEqual([cardFilterPill]); + }); + + it('generateStateFilterPills(): should add state filter pill', () => { + const state = ['DRAFT', 'READY_TO_REPORT', 'APPROVED']; + const filterPill = []; + myExpensesService.generateStateFilterPills(filterPill, { ...expenseFiltersData1, state }); + expect(filterPill).toEqual([stateFilterPill2]); + }); + + describe('convertSelectedSortFiltersToFilters(): ', () => { + it('should convert selected sort filter to corresponding sortParam and sortDir', () => { + const sortBy = { + name: 'Sort By', + value: 'dateNewToOld', + }; + const generatedFilters = {}; + + myExpensesService.convertSelectedSortFitlersToFilters(sortBy, generatedFilters); + + expect(generatedFilters).toEqual({ + sortParam: 'tx_txn_dt', + sortDir: 'desc', + }); + }); + + it('should convert selected sort filter to corresponding sortParam and sortDir (dateOldToNew)', () => { + const sortBy = { + name: 'Sort By', + value: 'dateOldToNew', + }; + const generatedFilters = {}; + + myExpensesService.convertSelectedSortFitlersToFilters(sortBy, generatedFilters); + + expect(generatedFilters).toEqual({ + sortParam: 'tx_txn_dt', + sortDir: 'asc', + }); + }); + + it('should convert selected sort filter to corresponding sortParam and sortDir (amountHighToLow)', () => { + const sortBy = { + name: 'Sort By', + value: 'amountHighToLow', + }; + const generatedFilters = {}; + + myExpensesService.convertSelectedSortFitlersToFilters(sortBy, generatedFilters); + + expect(generatedFilters).toEqual({ + sortParam: 'tx_amount', + sortDir: 'desc', + }); + }); + + it('should convert selected sort filter to corresponding sortParam and sortDir (amountLowToHigh)', () => { + const sortBy = { + name: 'Sort By', + value: 'amountLowToHigh', + }; + const generatedFilters = {}; + + myExpensesService.convertSelectedSortFitlersToFilters(sortBy, generatedFilters); + + expect(generatedFilters).toEqual({ + sortParam: 'tx_amount', + sortDir: 'asc', + }); + }); + + it('should convert selected sort filter to corresponding sortParam and sortDir (nameAToZ)', () => { + const sortBy = { + name: 'Sort By', + value: 'categoryAToZ', + }; + const generatedFilters = {}; + + myExpensesService.convertSelectedSortFitlersToFilters(sortBy, generatedFilters); + + expect(generatedFilters).toEqual({ + sortParam: 'tx_org_category', + sortDir: 'asc', + }); + }); + + it('should convert selected sort filter to corresponding sortParam and sortDir (nameZToA)', () => { + const sortBy = { + name: 'Sort By', + value: 'categoryZToA', + }; + const generatedFilters = {}; + + myExpensesService.convertSelectedSortFitlersToFilters(sortBy, generatedFilters); + + expect(generatedFilters).toEqual({ + sortParam: 'tx_org_category', + sortDir: 'desc', + }); + }); + }); + + it('getFilters(): should return all the filters', () => { + const filters = myExpensesService.getFilters(); + + expect(filters).toEqual(filterOptions2); + }); + + it('generateSelectedFilters(): should generate selected filters', () => { + spyOn(myExpensesService, 'addSortToGeneratedFilters'); + + const filters = myExpensesService.generateSelectedFilters(expenseFiltersData1); + + expect(myExpensesService.addSortToGeneratedFilters).toHaveBeenCalledOnceWith(expenseFiltersData1, filters); + + expect(filters).toEqual(selectedFilters9); + }); + + it('addSortToGeneratedFilters(): should call convertTxnDtSortToSelectedFilters, convertAmountSortToSelectedFilters and convertCategorySortToSelectedFilters once', () => { + spyOn(myExpensesService, 'convertTxnDtSortToSelectedFilters'); + spyOn(myExpensesService, 'convertAmountSortToSelectedFilters'); + spyOn(myExpensesService, 'convertCategorySortToSelectedFilters'); + + myExpensesService.addSortToGeneratedFilters(expenseFiltersData1, selectedFilters9); + + expect(myExpensesService.convertTxnDtSortToSelectedFilters).toHaveBeenCalledOnceWith( + expenseFiltersData1, + selectedFilters9 + ); + expect(myExpensesService.convertAmountSortToSelectedFilters).toHaveBeenCalledOnceWith( + expenseFiltersData1, + selectedFilters9 + ); + expect(myExpensesService.convertCategorySortToSelectedFilters).toHaveBeenCalledOnceWith( + expenseFiltersData1, + selectedFilters9 + ); + }); + + describe('convertCategorySortToSelectedFilters():', () => { + it('should add categoryAToZ sort params if sort direction is ascending', () => { + const generatedFilters = []; + + myExpensesService.convertCategorySortToSelectedFilters(expenseFiltersData1, generatedFilters); + + expect(generatedFilters).toEqual([ + { + name: 'Sort By', + value: 'categoryAToZ', + }, + ]); + }); + + it('should add categoryZToA sort params if sort direction is descending', () => { + const generatedFilters = []; + + myExpensesService.convertCategorySortToSelectedFilters( + { ...expenseFiltersData1, sortDir: 'desc' }, + generatedFilters + ); + + expect(generatedFilters).toEqual([ + { + name: 'Sort By', + value: 'categoryZToA', + }, + ]); + }); + }); + + describe('convertAmountSortToSelectedFilters(): ', () => { + it('should convert amount sort to selected filters for descending sort', () => { + const filter = { + sortParam: 'tx_amount', + sortDir: 'desc', + }; + const generatedFilters = []; + + myExpensesService.convertAmountSortToSelectedFilters(filter, generatedFilters); + + expect(generatedFilters).toEqual([ + { + name: 'Sort By', + value: 'amountHighToLow', + }, + ]); + }); + + it('should convert amount sort to selected filters for ascending sort', () => { + const filter = { + sortParam: 'tx_amount', + sortDir: 'asc', + }; + const generatedFilters = []; + + myExpensesService.convertAmountSortToSelectedFilters(filter, generatedFilters); + + expect(generatedFilters).toEqual([ + { + name: 'Sort By', + value: 'amountLowToHigh', + }, + ]); + }); + }); + + describe('convertTxnDtSortToSelectedFilters():', () => { + it('should covert txn date sort to selected filters for descending sort', () => { + const filter = { + sortParam: 'tx_txn_dt', + sortDir: 'desc', + }; + const generatedFilters = []; + + myExpensesService.convertTxnDtSortToSelectedFilters(filter, generatedFilters); + + expect(generatedFilters).toEqual([ + { + name: 'Sort By', + value: 'dateNewToOld', + }, + ]); + }); + + it('should covert txn date sort to selected filters for ascending sort', () => { + const filter = { + sortParam: 'tx_txn_dt', + sortDir: 'asc', + }; + const generatedFilters = []; + + myExpensesService.convertTxnDtSortToSelectedFilters(filter, generatedFilters); + + expect(generatedFilters).toEqual([ + { + name: 'Sort By', + value: 'dateOldToNew', + }, + ]); + }); + }); + + describe('generateSortCategoryPills():', () => { + it('should add category - a to z as sort params if sort direction is ascending', () => { + const filter = { + sortParam: 'tx_org_category', + sortDir: 'asc', + }; + const filterPill = []; + + //@ts-ignore + myExpensesService.generateSortCategoryPills(filter, filterPill); + + expect(filterPill).toEqual([sortFilterPill]); + }); + + it('should add category - z to a as sort params if sort direction is descending', () => { + const filter = { + sortParam: 'tx_org_category', + sortDir: 'desc', + }; + const filterPill = []; + + //@ts-ignore + myExpensesService.generateSortCategoryPills(filter, filterPill); + + expect(filterPill).toEqual([{ ...sortFilterPill, value: 'category - z to a' }]); + }); + }); +}); diff --git a/src/app/fyle/my-expenses/my-expenses.service.ts b/src/app/fyle/my-expenses/my-expenses.service.ts index 421ff80156..e8a398ac45 100644 --- a/src/app/fyle/my-expenses/my-expenses.service.ts +++ b/src/app/fyle/my-expenses/my-expenses.service.ts @@ -7,7 +7,6 @@ import { FilterOptionType } from 'src/app/shared/components/fy-filters/filter-op import { FilterOptions } from 'src/app/shared/components/fy-filters/filter-options.interface'; import { SelectedFilters } from 'src/app/shared/components/fy-filters/selected-filters.interface'; import { MaskNumber } from 'src/app/shared/pipes/mask-number.pipe'; -import { Filters } from './my-expenses-filters.model'; import { ExpenseFilters } from './expense-filters.model'; @Injectable({ @@ -16,7 +15,7 @@ import { ExpenseFilters } from './expense-filters.model'; export class MyExpensesService { maskNumber = new MaskNumber(); - generateSortFilterPills(filter, filterPills: FilterPill[]) { + generateSortFilterPills(filter: Partial, filterPills: FilterPill[]): void { this.generateSortTxnDatePills(filter, filterPills); this.generateSortAmountPills(filter, filterPills); @@ -70,7 +69,7 @@ export class MyExpensesService { return generatedFilters; } - generateSortAmountPills(filter: Filters, filterPills: FilterPill[]) { + generateSortAmountPills(filter: Partial, filterPills: FilterPill[]): void { if (filter.sortParam === 'tx_amount' && filter.sortDir === 'desc') { filterPills.push({ label: 'Sort By', @@ -86,7 +85,7 @@ export class MyExpensesService { } } - generateSortTxnDatePills(filter: Filters, filterPills: FilterPill[]) { + generateSortTxnDatePills(filter: Partial, filterPills: FilterPill[]): void { if (filter.sortParam === 'tx_txn_dt' && filter.sortDir === 'asc') { filterPills.push({ label: 'Sort By', @@ -102,7 +101,7 @@ export class MyExpensesService { } } - generateTypeFilterPills(filter: Partial, filterPills: FilterPill[]) { + generateTypeFilterPills(filter: Partial, filterPills: FilterPill[]): void { const combinedValue = filter.type .map((type) => { if (type === 'RegularExpenses') { @@ -124,7 +123,7 @@ export class MyExpensesService { }); } - generateDateFilterPills(filter: Partial, filterPills: FilterPill[]) { + generateDateFilterPills(filter: Partial, filterPills: FilterPill[]): FilterPill[] { let filterPillsCopy = cloneDeep(filterPills); if (filter.date === DateFilters.thisWeek) { filterPillsCopy.push({ @@ -165,7 +164,7 @@ export class MyExpensesService { return filterPillsCopy; } - generateCustomDatePill(filter: Partial, filterPills: FilterPill[]) { + generateCustomDatePill(filter: Partial, filterPills: FilterPill[]): FilterPill[] { const filterPillsCopy = cloneDeep(filterPills); const startDate = filter.customDateStart && dayjs(filter.customDateStart).format('YYYY-MM-D'); const endDate = filter.customDateEnd && dayjs(filter.customDateEnd).format('YYYY-MM-D'); @@ -193,7 +192,7 @@ export class MyExpensesService { return filterPillsCopy; } - generateReceiptsAttachedFilterPills(filterPills: FilterPill[], filter) { + generateReceiptsAttachedFilterPills(filterPills: FilterPill[], filter: Partial): void { filterPills.push({ label: 'Receipts Attached', type: 'receiptsAttached', @@ -201,7 +200,7 @@ export class MyExpensesService { }); } - generateSplitExpenseFilterPills(filterPills: FilterPill[], filter: Partial) { + generateSplitExpenseFilterPills(filterPills: FilterPill[], filter: Partial): void { filterPills.push({ label: 'Split Expense', type: 'splitExpense', @@ -209,7 +208,7 @@ export class MyExpensesService { }); } - generateCardFilterPills(filterPills: FilterPill[], filter) { + generateCardFilterPills(filterPills: FilterPill[], filter: Partial): void { filterPills.push({ label: 'Cards', type: 'cardNumbers', @@ -219,11 +218,13 @@ export class MyExpensesService { }); } - generateStateFilterPills(filterPills: FilterPill[], filter) { + generateStateFilterPills(filterPills: FilterPill[], filter: Partial): void { + const filterState = filter.state as string[]; + filterPills.push({ label: 'Type', type: 'state', - value: filter.state + value: filterState .map((state) => { if (state === 'DRAFT') { return 'Incomplete'; @@ -240,7 +241,7 @@ export class MyExpensesService { convertSelectedSortFitlersToFilters( sortBy: SelectedFilters, generatedFilters: Partial - ) { + ): void { if (sortBy) { if (sortBy.value === 'dateNewToOld') { generatedFilters.sortParam = 'tx_txn_dt'; @@ -449,7 +450,10 @@ export class MyExpensesService { return generatedFilters; } - addSortToGeneratedFilters(filter: Partial, generatedFilters: SelectedFilters[]) { + addSortToGeneratedFilters( + filter: Partial, + generatedFilters: SelectedFilters[] + ): void { this.convertTxnDtSortToSelectedFilters(filter, generatedFilters); this.convertAmountSortToSelectedFilters(filter, generatedFilters); @@ -460,7 +464,7 @@ export class MyExpensesService { convertCategorySortToSelectedFilters( filter: Partial, generatedFilters: SelectedFilters[] - ) { + ): void { if (filter.sortParam === 'tx_org_category' && filter.sortDir === 'asc') { generatedFilters.push({ name: 'Sort By', @@ -477,7 +481,7 @@ export class MyExpensesService { convertAmountSortToSelectedFilters( filter: Partial, generatedFilters: SelectedFilters[] - ) { + ): void { if (filter.sortParam === 'tx_amount' && filter.sortDir === 'desc') { generatedFilters.push({ name: 'Sort By', @@ -494,7 +498,7 @@ export class MyExpensesService { convertTxnDtSortToSelectedFilters( filter: Partial, generatedFilters: SelectedFilters[] - ) { + ): void { if (filter.sortParam === 'tx_txn_dt' && filter.sortDir === 'asc') { generatedFilters.push({ name: 'Sort By', @@ -508,7 +512,7 @@ export class MyExpensesService { } } - private generateSortCategoryPills(filter: Filters, filterPills: FilterPill[]) { + private generateSortCategoryPills(filter: Partial, filterPills: FilterPill[]): void { if (filter.sortParam === 'tx_org_category' && filter.sortDir === 'asc') { filterPills.push({ label: 'Sort By', From e7f13e4307fa646ed03a2f1c9cadb84c34321d57 Mon Sep 17 00:00:00 2001 From: Suyash Patil <127177049+suyashpatil78@users.noreply.github.com> Date: Tue, 10 Oct 2023 17:30:59 +0530 Subject: [PATCH 11/14] test: 35% coverage of view-team-advance-request page (#2492) * test: 35% coverage of view-team-adv-req * minor * test: 66% coverage of view-team-adv-request page (#2493) * test: 66% coverage of view-team-adv-request page * coverage at 74 * minor * minor * test: 100% coverage of view-team-adv-request page: (#2494) --- src/app/core/mock-data/file-object.data.ts | 9 + .../core/mock-data/modal-controller.data.ts | 71 ++- src/app/core/mock-data/popup.data.ts | 8 + .../view-team-advance-request.page.html | 2 +- .../view-team-advance-request.page.spec.ts | 488 +++++++++++++++++- .../view-team-advance-request.page.ts | 64 ++- 6 files changed, 598 insertions(+), 44 deletions(-) diff --git a/src/app/core/mock-data/file-object.data.ts b/src/app/core/mock-data/file-object.data.ts index b6f37c79d9..c59d2fa16e 100644 --- a/src/app/core/mock-data/file-object.data.ts +++ b/src/app/core/mock-data/file-object.data.ts @@ -358,3 +358,12 @@ export const fileObject9: FileObject[] = [ fyle_sub_url: '/api/files/fiV1gXpyCcbU/download', }, ]; + +export const fileObject10: FileObject[] = [ + { + ...fileObjectAdv1, + url: 'mockdownloadurl.png', + type: 'pdf', + thumbnail: 'src/assets/images/pdf-receipt-placeholder.png', + }, +]; diff --git a/src/app/core/mock-data/modal-controller.data.ts b/src/app/core/mock-data/modal-controller.data.ts index a9b57276d1..e22e8ece25 100644 --- a/src/app/core/mock-data/modal-controller.data.ts +++ b/src/app/core/mock-data/modal-controller.data.ts @@ -17,8 +17,9 @@ import { FyInputPopoverComponent } from 'src/app/shared/components/fy-input-popo import { PolicyViolationDialogComponent } from 'src/app/fyle/add-edit-advance-request/policy-violation-dialog/policy-violation-dialog.component'; import { CaptureReceiptComponent } from 'src/app/shared/components/capture-receipt/capture-receipt.component'; import { FyViewAttachmentComponent } from 'src/app/shared/components/fy-view-attachment/fy-view-attachment.component'; -import { advanceRequestFileUrlData2 } from './file-object.data'; +import { advanceRequestFileUrlData2, fileObject4 } from './file-object.data'; import { ViewCommentComponent } from 'src/app/shared/components/comments-history/view-comment/view-comment.component'; +import { FyPopoverComponent } from 'src/app/shared/components/fy-popover/fy-popover.component'; export const modalControllerParams = { component: FyFiltersComponent, @@ -444,3 +445,71 @@ export const modalControllerParams5 = { breakpoints: [0, 1], handle: false, }; + +export const popoverControllerParams5 = { + component: PopupAlertComponent, + cssClass: 'pop-up-in-center', + componentProps: { + title: 'Review Advance', + message: 'Advance request by Abhishek Jain of amount $54 will be approved', + primaryCta: { + text: 'Approve', + action: 'approve', + }, + secondaryCta: { + text: 'Cancel', + action: 'cancel', + }, + }, +}; + +export const popoverControllerParams6 = { + component: FyPopoverComponent, + cssClass: 'fy-dialog-popover', + componentProps: { + title: 'Send Back', + formLabel: 'Reason For Sending Back Advance', + }, +}; + +export const popoverControllerParams7 = { + component: FyPopoverComponent, + cssClass: 'fy-dialog-popover', + componentProps: { + title: 'Reject', + formLabel: 'Please mention the reason for rejecting the advance request', + }, +}; + +export const modalControllerParams6 = { + component: ViewCommentComponent, + componentProps: { + objectType: 'advance_requests', + objectId: 'areqR1cyLgXdND', + }, + cssClass: 'fy-modal', + showBackdrop: true, + canDismiss: true, + backdropDismiss: true, + animated: true, + initialBreakpoint: 1, + breakpoints: [0, 1], + handle: false, +}; + +export const modalControllerParams7 = { + component: FyViewAttachmentComponent, + componentProps: { + attachments: fileObject4[0], + }, + mode: 'ios' as Mode, + presentingElement: undefined, + cssClass: 'fy-modal', + showBackdrop: true, + canDismiss: true, + backdropDismiss: true, + animated: true, + initialBreakpoint: 1, + breakpoints: [0, 1], + handle: false, +}; diff --git a/src/app/core/mock-data/popup.data.ts b/src/app/core/mock-data/popup.data.ts index ab6f4d409e..d7ed61d21e 100644 --- a/src/app/core/mock-data/popup.data.ts +++ b/src/app/core/mock-data/popup.data.ts @@ -22,3 +22,11 @@ export const popupConfigData2: PopupConfig = { text: 'Close', }, }; + +export const popupConfigData3: PopupConfig = { + header: 'Confirm', + message: 'Are you sure you want to delete this Advance Request', + primaryCta: { + text: 'Delete Advance Request', + }, +}; diff --git a/src/app/fyle/view-team-advance-request/view-team-advance-request.page.html b/src/app/fyle/view-team-advance-request/view-team-advance-request.page.html index d21b99fb02..93545116f5 100644 --- a/src/app/fyle/view-team-advance-request/view-team-advance-request.page.html +++ b/src/app/fyle/view-team-advance-request/view-team-advance-request.page.html @@ -5,7 +5,7 @@ slot="start" [ngClass]="{'view-team-advance-request--header-btn-container__sm': isDeviceWidthSmall}" > - + diff --git a/src/app/fyle/view-team-advance-request/view-team-advance-request.page.spec.ts b/src/app/fyle/view-team-advance-request/view-team-advance-request.page.spec.ts index 5e3f835931..8b3d50444a 100644 --- a/src/app/fyle/view-team-advance-request/view-team-advance-request.page.spec.ts +++ b/src/app/fyle/view-team-advance-request/view-team-advance-request.page.spec.ts @@ -1,26 +1,486 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { IonicModule } from '@ionic/angular'; +import { ComponentFixture, TestBed, fakeAsync, tick, waitForAsync } from '@angular/core/testing'; +import { ActionSheetController, IonicModule, ModalController, NavController, PopoverController } from '@ionic/angular'; import { ViewTeamAdvanceRequestPage } from './view-team-advance-request.page'; +import { AdvanceRequestService } from 'src/app/core/services/advance-request.service'; +import { FileService } from 'src/app/core/services/file.service'; +import { ActivatedRoute, Router, UrlSerializer } from '@angular/router'; +import { PopupService } from 'src/app/core/services/popup.service'; +import { LoaderService } from 'src/app/core/services/loader.service'; +import { AdvanceRequestsCustomFieldsService } from 'src/app/core/services/advance-requests-custom-fields.service'; +import { AuthService } from 'src/app/core/services/auth.service'; +import { ModalPropertiesService } from 'src/app/core/services/modal-properties.service'; +import { TrackingService } from 'src/app/core/services/tracking.service'; +import { ExpenseFieldsService } from 'src/app/core/services/expense-fields.service'; +import { HumanizeCurrencyPipe } from 'src/app/shared/pipes/humanize-currency.pipe'; +import { MIN_SCREEN_WIDTH } from 'src/app/app.module'; +import { StatisticTypes } from 'src/app/shared/components/fy-statistic/statistic-type.enum'; +import { transformedResponse2 } from 'src/app/core/mock-data/expense-field.data'; +import { Subject, finalize, of } from 'rxjs'; +import { singleExtendedAdvancesData } from 'src/app/core/mock-data/extended-advance.data'; +import { extendedAdvReqDraft, myAdvanceRequestsData2 } from 'src/app/core/mock-data/extended-advance-request.data'; +import { apiAdvanceRequestAction } from 'src/app/core/mock-data/advance-request-actions.data'; +import { advanceReqApprovals } from 'src/app/core/mock-data/approval.data'; +import { advanceRequestFileUrlData, fileObject10, fileObject4 } from 'src/app/core/mock-data/file-object.data'; +import { advanceRequestCustomFieldData2 } from 'src/app/core/mock-data/advance-requests-custom-fields.data'; +import { apiEouRes } from 'src/app/core/mock-data/extended-org-user.data'; +import { customFields } from 'src/app/core/mock-data/custom-field.data'; +import { cloneDeep } from 'lodash'; +import { CustomField } from 'src/app/core/models/custom_field.model'; +import { popupConfigData3 } from 'src/app/core/mock-data/popup.data'; +import { advanceRequests } from 'src/app/core/mock-data/advance-requests.data'; +import { + modalControllerParams6, + modalControllerParams7, + popoverControllerParams5, + popoverControllerParams6, + popoverControllerParams7, +} from 'src/app/core/mock-data/modal-controller.data'; +import { properties } from 'src/app/core/mock-data/modal-properties.data'; -xdescribe('ViewTeamAdvanceRequestPage', () => { +describe('ViewTeamAdvanceRequestPage', () => { let component: ViewTeamAdvanceRequestPage; let fixture: ComponentFixture; + let advanceRequestService: jasmine.SpyObj; + let fileService: jasmine.SpyObj; + let router: jasmine.SpyObj; + let popupService: jasmine.SpyObj; + let popoverController: jasmine.SpyObj; + let actionSheetController: jasmine.SpyObj; + let loaderService: jasmine.SpyObj; + let advanceRequestsCustomFieldsService: jasmine.SpyObj; + let authService: jasmine.SpyObj; + let modalController: jasmine.SpyObj; + let modalProperties: jasmine.SpyObj; + let trackingService: jasmine.SpyObj; + let expenseFieldsService: jasmine.SpyObj; + let humanizeCurrency: jasmine.SpyObj; + let activatedRoute: jasmine.SpyObj; - beforeEach( - waitForAsync(() => { - TestBed.configureTestingModule({ - declarations: [ViewTeamAdvanceRequestPage], - imports: [IonicModule.forRoot()], - }).compileComponents(); + beforeEach(waitForAsync(() => { + const advanceRequestServiceSpy = jasmine.createSpyObj('AdvanceRequestService', [ + 'getAdvanceRequest', + 'getActions', + 'getActiveApproversByAdvanceRequestId', + 'modifyAdvanceRequestCustomFields', + 'delete', + 'approve', + 'sendBack', + 'reject', + ]); + const fileServiceSpy = jasmine.createSpyObj('FileService', [ + 'findByAdvanceRequestId', + 'downloadUrl', + 'getReceiptsDetails', + '', + ]); + const routerSpy = jasmine.createSpyObj('Router', ['navigate']); + const popupServiceSpy = jasmine.createSpyObj('PopupService', ['showPopup']); + const popoverControllerSpy = jasmine.createSpyObj('PopoverController', ['create']); + const actionSheetControllerSpy = jasmine.createSpyObj('ActionSheetController', ['create']); + const loaderServiceSpy = jasmine.createSpyObj('LoaderService', ['showLoader', 'hideLoader']); + const advanceRequestsCustomFieldsServiceSpy = jasmine.createSpyObj('AdvanceRequestsCustomFieldsService', [ + 'getAll', + ]); + const authServiceSpy = jasmine.createSpyObj('AuthService', ['getEou']); + const modalControllerSpy = jasmine.createSpyObj('ModalController', ['create', 'getTop']); + const modalPropertiesSpy = jasmine.createSpyObj('ModalPropertiesService', ['getModalDefaultProperties']); + const trackingServiceSpy = jasmine.createSpyObj('TrackingService', [ + 'sendBackAdvance', + 'rejectAdvance', + 'addComment', + 'viewComment', + ]); + const expenseFieldsServiceSpy = jasmine.createSpyObj('ExpenseFieldsService', ['getAllEnabled']); + const humanizeCurrencySpy = jasmine.createSpyObj('HumanizeCurrencyPipe', ['transform']); - fixture = TestBed.createComponent(ViewTeamAdvanceRequestPage); - component = fixture.componentInstance; - fixture.detectChanges(); - }) - ); + TestBed.configureTestingModule({ + declarations: [ViewTeamAdvanceRequestPage], + imports: [IonicModule.forRoot()], + providers: [ + { provide: AdvanceRequestService, useValue: advanceRequestServiceSpy }, + { provide: FileService, useValue: fileServiceSpy }, + { provide: Router, useValue: routerSpy }, + { provide: PopupService, useValue: popupServiceSpy }, + { provide: PopoverController, useValue: popoverControllerSpy }, + { provide: ActionSheetController, useValue: actionSheetControllerSpy }, + { provide: LoaderService, useValue: loaderServiceSpy }, + { provide: AdvanceRequestsCustomFieldsService, useValue: advanceRequestsCustomFieldsServiceSpy }, + { provide: AuthService, useValue: authServiceSpy }, + { provide: ModalController, useValue: modalControllerSpy }, + { provide: ModalPropertiesService, useValue: modalPropertiesSpy }, + { provide: TrackingService, useValue: trackingServiceSpy }, + { provide: ExpenseFieldsService, useValue: expenseFieldsServiceSpy }, + { provide: HumanizeCurrencyPipe, useValue: humanizeCurrencySpy }, + { + provide: ActivatedRoute, + useValue: { + snapshot: { + params: { + id: 'areqR1cyLgXdND', + }, + }, + }, + }, + { + provide: MIN_SCREEN_WIDTH, + useValue: 230, + }, + UrlSerializer, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(ViewTeamAdvanceRequestPage); + component = fixture.componentInstance; + + advanceRequestService = TestBed.inject(AdvanceRequestService) as jasmine.SpyObj; + fileService = TestBed.inject(FileService) as jasmine.SpyObj; + router = TestBed.inject(Router) as jasmine.SpyObj; + popupService = TestBed.inject(PopupService) as jasmine.SpyObj; + popoverController = TestBed.inject(PopoverController) as jasmine.SpyObj; + actionSheetController = TestBed.inject(ActionSheetController) as jasmine.SpyObj; + loaderService = TestBed.inject(LoaderService) as jasmine.SpyObj; + advanceRequestsCustomFieldsService = TestBed.inject( + AdvanceRequestsCustomFieldsService + ) as jasmine.SpyObj; + authService = TestBed.inject(AuthService) as jasmine.SpyObj; + modalController = TestBed.inject(ModalController) as jasmine.SpyObj; + modalProperties = TestBed.inject(ModalPropertiesService) as jasmine.SpyObj; + trackingService = TestBed.inject(TrackingService) as jasmine.SpyObj; + expenseFieldsService = TestBed.inject(ExpenseFieldsService) as jasmine.SpyObj; + humanizeCurrency = TestBed.inject(HumanizeCurrencyPipe) as jasmine.SpyObj; + activatedRoute = TestBed.inject(ActivatedRoute) as jasmine.SpyObj; + fixture.detectChanges(); + })); it('should create', () => { expect(component).toBeTruthy(); }); + + it('StatisticTypes(): should return statistic types', () => { + expect(component.StatisticTypes).toEqual(StatisticTypes); + }); + + it('getAndUpdateProjectName(): should return expense field with column name as project_id', fakeAsync(() => { + expenseFieldsService.getAllEnabled.and.returnValue(of(transformedResponse2)); + + const res = component.getAndUpdateProjectName(); + tick(100); + + res.then((data) => { + expect(data).toEqual(transformedResponse2[0]); + }); + })); + + describe('ionViewWillEnter():', () => { + beforeEach(() => { + loaderService.showLoader.and.resolveTo(); + loaderService.hideLoader.and.resolveTo(); + advanceRequestService.getAdvanceRequest.and.returnValue(of(extendedAdvReqDraft)); + advanceRequestService.getActions.and.returnValue(of(apiAdvanceRequestAction)); + advanceRequestService.getActiveApproversByAdvanceRequestId.and.returnValue(of(advanceReqApprovals)); + spyOn(component, 'getAttachedReceipts').and.returnValue(of(fileObject4)); + advanceRequestsCustomFieldsService.getAll.and.returnValue(of(advanceRequestCustomFieldData2)); + authService.getEou.and.resolveTo(apiEouRes); + advanceRequestService.modifyAdvanceRequestCustomFields.and.returnValue(customFields); + advanceRequestService.modifyAdvanceRequestCustomFields.and.returnValue(customFields); + spyOn(component, 'setupActionSheet'); + spyOn(component, 'getAndUpdateProjectName').and.resolveTo(transformedResponse2[0]); + }); + + it('should set advanceRequest$, actions$ and showAdvanceActions$ correctly', fakeAsync(() => { + component.ionViewWillEnter(); + tick(100); + + component.advanceRequest$ + .pipe( + finalize(() => { + expect(loaderService.hideLoader).toHaveBeenCalledTimes(1); + }) + ) + .subscribe((data) => { + expect(loaderService.showLoader).toHaveBeenCalledTimes(1); + expect(data).toEqual(extendedAdvReqDraft); + expect(advanceRequestService.getAdvanceRequest).toHaveBeenCalledOnceWith('areqR1cyLgXdND'); + }); + + component.actions$.subscribe((data) => { + expect(data).toEqual(apiAdvanceRequestAction); + expect(advanceRequestService.getActions).toHaveBeenCalledOnceWith('areqR1cyLgXdND'); + }); + + component.showAdvanceActions$.subscribe((data) => { + expect(data).toEqual(false); + }); + })); + + it('should set approvals$, activeApprovals$, attachedFiles$ and customFields$ correctly', fakeAsync(() => { + component.ionViewWillEnter(); + tick(100); + + component.approvals$.subscribe((data) => { + expect(data).toEqual(advanceReqApprovals); + expect(advanceRequestService.getActiveApproversByAdvanceRequestId).toHaveBeenCalledOnceWith('areqR1cyLgXdND'); + }); + + component.activeApprovals$.subscribe((data) => { + expect(data).toEqual(advanceReqApprovals); + }); + + component.attachedFiles$.subscribe((data) => { + expect(data).toEqual(fileObject4); + expect(component.getAttachedReceipts).toHaveBeenCalledOnceWith('areqR1cyLgXdND'); + }); + + component.customFields$.subscribe((data) => { + expect(data).toEqual(advanceRequestCustomFieldData2); + expect(advanceRequestsCustomFieldsService.getAll).toHaveBeenCalledTimes(1); + }); + })); + + it('should set advanceRequestCustomFields$ equal to custom fields returned by advanceRequestsCustomFieldsService.getAll', fakeAsync(() => { + const mockCustomField = cloneDeep(advanceRequestCustomFieldData2); + advanceRequestsCustomFieldsService.getAll.and.returnValue(of(mockCustomField)); + + component.ionViewWillEnter(); + tick(100); + + component.advanceRequestCustomFields$.subscribe((data) => { + expect(data).toEqual(mockCustomField); + expect(advanceRequestsCustomFieldsService.getAll).toHaveBeenCalledTimes(1); + }); + })); + + it('should set advanceRequestCustomFields$ equal to value returned by advanceRequestService.modifyAdvanceRequestCustomFields if user org id is not equal to advance request org id', fakeAsync(() => { + const eouRes = cloneDeep(apiEouRes); + eouRes.ou.org_id = 'or2390Fjsd'; + authService.getEou.and.resolveTo(eouRes); + let customField: CustomField[] = JSON.parse(extendedAdvReqDraft.areq_custom_field_values); + component.ionViewWillEnter(); + tick(100); + + component.advanceRequestCustomFields$.subscribe((data) => { + expect(data).toEqual(customFields); + expect(advanceRequestService.modifyAdvanceRequestCustomFields).toHaveBeenCalledOnceWith(customField); + }); + })); + + it('should call setupActionScheet() and update projectFieldName correctly', fakeAsync(() => { + component.ionViewWillEnter(); + tick(100); + + expect(component.setupActionSheet).toHaveBeenCalledTimes(1); + expect(component.getAndUpdateProjectName).toHaveBeenCalledTimes(1); + expect(component.projectFieldName).toEqual('Purpose'); + })); + }); + + it('getAttachedReceipts(): should return all the attached receipts corresponding to an advance request', () => { + const mockFileObject = cloneDeep(advanceRequestFileUrlData[0]); + fileService.getReceiptsDetails.and.returnValue({ + type: 'pdf', + thumbnail: 'src/assets/images/pdf-receipt-placeholder.png', + }); + fileService.downloadUrl.and.returnValue(of('mockdownloadurl.png')); + fileService.findByAdvanceRequestId.and.returnValue(of([mockFileObject])); + component.getAttachedReceipts('areqR1cyLgXdND').subscribe((res) => { + expect(fileService.getReceiptsDetails).toHaveBeenCalledOnceWith(mockFileObject); + expect(fileService.downloadUrl).toHaveBeenCalledOnceWith('fiSSsy2Bf4Se'); + expect(fileService.findByAdvanceRequestId).toHaveBeenCalledOnceWith('areqR1cyLgXdND'); + expect(res).toEqual(fileObject10); + }); + }); + + it('edit(): should navigate to add edit advance request page with params.from as TEAM_ADVANCE', () => { + component.edit(); + expect(router.navigate).toHaveBeenCalledOnceWith([ + '/', + 'enterprise', + 'add_edit_advance_request', + { + id: 'areqR1cyLgXdND', + from: 'TEAM_ADVANCE', + }, + ]); + }); + + describe('getApproverEmails():', () => { + it('getApproverEmails(): should return approver emails', () => { + const approvalEmails = component.getApproverEmails(advanceReqApprovals); + expect(approvalEmails).toEqual(['ajain@fyle.in']); + }); + + it('getApproverEmails(): should return undefined if approvals are undefined', () => { + const approvalEmails = component.getApproverEmails(undefined); + expect(approvalEmails).toEqual(undefined); + }); + }); + + it('onUpdateApprover(): should refresh approvers', () => { + spyOn(component.refreshApprovers$, 'next'); + component.onUpdateApprover(true); + expect(component.refreshApprovers$.next).toHaveBeenCalledOnceWith(null); + }); + + it('delete(): should show delete popup and navigate to team_advance page', fakeAsync(() => { + popupService.showPopup.and.resolveTo('primary'); + advanceRequestService.delete.and.returnValue(of(advanceRequests)); + + component.delete(); + tick(100); + + expect(popupService.showPopup).toHaveBeenCalledOnceWith(popupConfigData3); + expect(advanceRequestService.delete).toHaveBeenCalledOnceWith('areqR1cyLgXdND'); + expect(router.navigate).toHaveBeenCalledOnceWith(['/', 'enterprise', 'team_advance']); + })); + + it('setupActionSheet(): should populate actionSheetButtons', fakeAsync(() => { + spyOn(component, 'showApproveAdvanceSummaryPopover'); + spyOn(component, 'showSendBackAdvanceSummaryPopover'); + spyOn(component, 'showRejectAdvanceSummaryPopup'); + const mockActions = cloneDeep(apiAdvanceRequestAction); + mockActions.can_approve = true; + mockActions.can_inquire = true; + mockActions.can_reject = true; + component.actions$ = of(mockActions); + component.setupActionSheet(); + + component.actionSheetButtons.forEach((button) => { + button.handler(); + }); + expect(component.showApproveAdvanceSummaryPopover).toHaveBeenCalledTimes(1); + expect(component.showSendBackAdvanceSummaryPopover).toHaveBeenCalledTimes(1); + expect(component.showRejectAdvanceSummaryPopup).toHaveBeenCalledTimes(1); + expect(component.actionSheetButtons[0].text).toEqual('Approve Advance'); + expect(component.actionSheetButtons[1].text).toEqual('Send Back Advance'); + expect(component.actionSheetButtons[2].text).toEqual('Reject Advance'); + })); + + it('openActionSheet(): should call actionSheetController.create with correct params', fakeAsync(() => { + const actionSheetSpy = jasmine.createSpyObj('ActionSheet', ['present']); + actionSheetController.create.and.returnValue(actionSheetSpy); + component.openActionSheet(); + tick(100); + expect(actionSheetController.create).toHaveBeenCalledOnceWith({ + header: 'ADVANCE ACTIONS', + mode: 'md', + cssClass: 'fy-action-sheet advances-action-sheet', + buttons: component.actionSheetButtons, + }); + expect(actionSheetSpy.present).toHaveBeenCalledTimes(1); + })); + + it('showApproveAdvanceSummaryPopover(): should show popup for approving advances', fakeAsync(() => { + component.advanceRequest$ = of(extendedAdvReqDraft); + humanizeCurrency.transform.and.returnValue('$54'); + const showApproverSpy = jasmine.createSpyObj('showApprover', ['present', 'onWillDismiss']); + showApproverSpy.onWillDismiss.and.resolveTo({ data: { action: 'approve' } }); + popoverController.create.and.resolveTo(showApproverSpy); + advanceRequestService.approve.and.returnValue(of(advanceRequests)); + + component.showApproveAdvanceSummaryPopover(); + tick(100); + + expect(popoverController.create).toHaveBeenCalledOnceWith(popoverControllerParams5); + expect(showApproverSpy.present).toHaveBeenCalledTimes(1); + expect(showApproverSpy.onWillDismiss).toHaveBeenCalledTimes(1); + expect(humanizeCurrency.transform).toHaveBeenCalledOnceWith(54, 'USD', false); + expect(advanceRequestService.approve).toHaveBeenCalledOnceWith('areqoVuT5I8OOy'); + expect(router.navigate).toHaveBeenCalledOnceWith(['/', 'enterprise', 'team_advance']); + })); + + it('showSendBackAdvanceSummaryPopover(): should show popup for sending back advances', fakeAsync(() => { + const showApproverSpy = jasmine.createSpyObj('showApprover', ['present', 'onWillDismiss']); + showApproverSpy.onWillDismiss.and.resolveTo({ data: { comment: 'comment' } }); + popoverController.create.and.resolveTo(showApproverSpy); + advanceRequestService.sendBack.and.returnValue(of(advanceRequests)); + + component.showSendBackAdvanceSummaryPopover(); + tick(100); + + expect(popoverController.create).toHaveBeenCalledOnceWith(popoverControllerParams6); + expect(showApproverSpy.present).toHaveBeenCalledTimes(1); + expect(showApproverSpy.onWillDismiss).toHaveBeenCalledTimes(1); + expect(advanceRequestService.sendBack).toHaveBeenCalledOnceWith('areqR1cyLgXdND', { + status: { + comment: 'comment', + }, + notify: false, + }); + expect(router.navigate).toHaveBeenCalledOnceWith(['/', 'enterprise', 'team_advance']); + expect(trackingService.sendBackAdvance).toHaveBeenCalledOnceWith({ Asset: 'Mobile' }); + })); + + it('showRejectAdvanceSummaryPopup(): should show popup for rejecting advances', fakeAsync(() => { + const showApproverSpy = jasmine.createSpyObj('showApprover', ['present', 'onWillDismiss']); + showApproverSpy.onWillDismiss.and.resolveTo({ data: { comment: 'comment' } }); + popoverController.create.and.resolveTo(showApproverSpy); + advanceRequestService.reject.and.returnValue(of(advanceRequests)); + + component.showRejectAdvanceSummaryPopup(); + tick(100); + + expect(popoverController.create).toHaveBeenCalledOnceWith(popoverControllerParams7); + expect(showApproverSpy.present).toHaveBeenCalledTimes(1); + expect(showApproverSpy.onWillDismiss).toHaveBeenCalledTimes(1); + expect(advanceRequestService.reject).toHaveBeenCalledOnceWith('areqR1cyLgXdND', { + status: { + comment: 'comment', + }, + notify: false, + }); + expect(router.navigate).toHaveBeenCalledOnceWith(['/', 'enterprise', 'team_advance']); + expect(trackingService.rejectAdvance).toHaveBeenCalledOnceWith({ Asset: 'Mobile' }); + })); + + describe('openCommentsModal():', () => { + let modalSpy: jasmine.SpyObj; + beforeEach(() => { + modalSpy = jasmine.createSpyObj('modal', ['present', 'onDidDismiss']); + modalSpy.onDidDismiss.and.resolveTo({ data: { updated: false } }); + modalController.create.and.resolveTo(modalSpy); + modalProperties.getModalDefaultProperties.and.returnValue(properties); + }); + + it('should open comments modal and track viewComment event if updated if false', fakeAsync(() => { + component.openCommentsModal(); + tick(100); + + expect(modalController.create).toHaveBeenCalledOnceWith(modalControllerParams6); + expect(modalSpy.present).toHaveBeenCalledTimes(1); + expect(modalSpy.onDidDismiss).toHaveBeenCalledTimes(1); + expect(trackingService.viewComment).toHaveBeenCalledTimes(1); + expect(trackingService.addComment).not.toHaveBeenCalled(); + })); + + it('should open comments modal and track addComment event if updated if true', fakeAsync(() => { + modalSpy.onDidDismiss.and.resolveTo({ data: { updated: true } }); + modalController.create.and.resolveTo(modalSpy); + component.openCommentsModal(); + tick(100); + + expect(modalController.create).toHaveBeenCalledOnceWith(modalControllerParams6); + expect(modalSpy.present).toHaveBeenCalledTimes(1); + expect(modalSpy.onDidDismiss).toHaveBeenCalledTimes(1); + expect(trackingService.addComment).toHaveBeenCalledTimes(1); + expect(trackingService.viewComment).not.toHaveBeenCalled(); + })); + }); + + it('viewAttachments(): should open attachments modal', fakeAsync(() => { + const modalSpy = jasmine.createSpyObj('modal', ['present']); + modalController.create.and.resolveTo(modalSpy); + modalController.getTop.and.resolveTo(undefined); + modalProperties.getModalDefaultProperties.and.returnValue(properties); + + component.viewAttachments(fileObject4[0]); + tick(100); + expect(modalController.create).toHaveBeenCalledOnceWith(modalControllerParams7); + expect(modalSpy.present).toHaveBeenCalledTimes(1); + })); + + it('goToTeamAdvances(): should navigate to team_advance page', () => { + component.goToTeamAdvances(); + expect(router.navigate).toHaveBeenCalledOnceWith(['/', 'enterprise', 'team_advance']); + }); }); diff --git a/src/app/fyle/view-team-advance-request/view-team-advance-request.page.ts b/src/app/fyle/view-team-advance-request/view-team-advance-request.page.ts index 58b00d2900..b730446625 100644 --- a/src/app/fyle/view-team-advance-request/view-team-advance-request.page.ts +++ b/src/app/fyle/view-team-advance-request/view-team-advance-request.page.ts @@ -46,7 +46,7 @@ export class ViewTeamAdvanceRequestPage implements OnInit { attachedFiles$: Observable; - advanceRequestCustomFields$: Observable; + advanceRequestCustomFields$: Observable; refreshApprovers$ = new Subject(); @@ -82,7 +82,7 @@ export class ViewTeamAdvanceRequestPage implements OnInit { private trackingService: TrackingService, private expenseFieldsService: ExpenseFieldsService, private humanizeCurrency: HumanizeCurrencyPipe, - @Inject(MIN_SCREEN_WIDTH) public minScreenWidth: number, + @Inject(MIN_SCREEN_WIDTH) public minScreenWidth: number ) {} get StatisticTypes(): typeof StatisticTypes { @@ -94,21 +94,39 @@ export class ViewTeamAdvanceRequestPage implements OnInit { return expenseFields.filter((expenseField) => expenseField.column_name === 'project_id')[0]; } + getAttachedReceipts(id: string): Observable { + return this.fileService.findByAdvanceRequestId(id).pipe( + switchMap((res) => from(res)), + concatMap((fileObj: FileObject) => + this.fileService.downloadUrl(fileObj.id).pipe( + map((downloadUrl) => { + fileObj.url = downloadUrl; + const details = this.fileService.getReceiptsDetails(fileObj); + fileObj.type = details.type; + fileObj.thumbnail = details.thumbnail; + return fileObj; + }) + ) + ), + reduce((acc, curr) => acc.concat(curr), [] as FileObject[]) + ); + } + ionViewWillEnter(): void { const id = this.activatedRoute.snapshot.params.id as string; this.advanceRequest$ = this.refreshApprovers$.pipe( startWith(true), switchMap(() => - from(this.loaderService.showLoader()).pipe(switchMap(() => this.advanceRequestService.getAdvanceRequest(id))), + from(this.loaderService.showLoader()).pipe(switchMap(() => this.advanceRequestService.getAdvanceRequest(id))) ), finalize(() => from(this.loaderService.hideLoader())), - shareReplay(1), + shareReplay(1) ); this.actions$ = this.advanceRequestService.getActions(id).pipe(shareReplay(1)); this.showAdvanceActions$ = this.actions$.pipe( - map((advanceActions) => advanceActions.can_approve || advanceActions.can_inquire || advanceActions.can_reject), + map((advanceActions) => advanceActions.can_approve || advanceActions.can_inquire || advanceActions.can_reject) ); this.approvals$ = this.advanceRequestService.getActiveApproversByAdvanceRequestId(id); @@ -116,24 +134,10 @@ export class ViewTeamAdvanceRequestPage implements OnInit { this.activeApprovals$ = this.refreshApprovers$.pipe( startWith(true), switchMap(() => this.approvals$), - map((approvals) => approvals.filter((approval) => approval.state !== 'APPROVAL_DISABLED')), + map((approvals) => approvals.filter((approval) => approval.state !== 'APPROVAL_DISABLED')) ); - this.attachedFiles$ = this.fileService.findByAdvanceRequestId(id).pipe( - switchMap((res) => from(res)), - concatMap((fileObj: FileObject) => - this.fileService.downloadUrl(fileObj.id).pipe( - map((downloadUrl) => { - fileObj.url = downloadUrl; - const details = this.fileService.getReceiptsDetails(fileObj); - fileObj.type = details.type; - fileObj.thumbnail = details.thumbnail; - return fileObj; - }), - ), - ), - reduce((acc, curr) => acc.concat(curr), [] as FileObject[]), - ); + this.attachedFiles$ = this.getAttachedReceipts(id); this.customFields$ = this.advanceRequestsCustomFieldsService.getAll(); @@ -150,7 +154,7 @@ export class ViewTeamAdvanceRequestPage implements OnInit { res.advanceRequest.areq_custom_field_values.length > 0 ) { customFieldValues = this.advanceRequestService.modifyAdvanceRequestCustomFields( - JSON.parse(res.advanceRequest.areq_custom_field_values) as CustomField[], + JSON.parse(res.advanceRequest.areq_custom_field_values) as CustomField[] ); } @@ -164,14 +168,14 @@ export class ViewTeamAdvanceRequestPage implements OnInit { return res.customFields; } else { return this.advanceRequestService.modifyAdvanceRequestCustomFields( - JSON.parse(res.advanceRequest.areq_custom_field_values) as CustomField[], + JSON.parse(res.advanceRequest.areq_custom_field_values) as CustomField[] ); } - }), + }) ); this.advanceRequestCustomFields$ = customFields$; - this.setupActionScheet(); + this.setupActionSheet(); this.getAndUpdateProjectName().then((projectField) => (this.projectFieldName = projectField.field_name)); } @@ -215,7 +219,7 @@ export class ViewTeamAdvanceRequestPage implements OnInit { } } - setupActionScheet(): void { + setupActionSheet(): void { this.actions$.subscribe((actions) => { if (actions.can_approve) { this.actionSheetButtons.push({ @@ -331,7 +335,7 @@ export class ViewTeamAdvanceRequestPage implements OnInit { finalize(() => { this.sendBackLoading = false; this.trackingService.sendBackAdvance({ Asset: 'Mobile' }); - }), + }) ) .subscribe(() => { this.router.navigate(['/', 'enterprise', 'team_advance']); @@ -373,7 +377,7 @@ export class ViewTeamAdvanceRequestPage implements OnInit { finalize(() => { this.rejectLoading = false; this.trackingService.rejectAdvance({ Asset: 'Mobile' }); - }), + }) ) .subscribe(() => { this.router.navigate(['/', 'enterprise', 'team_advance']); @@ -423,4 +427,8 @@ export class ViewTeamAdvanceRequestPage implements OnInit { ngOnInit(): void { return; } + + goToTeamAdvances(): void { + this.router.navigate(['/', 'enterprise', 'team_advance']); + } } From d510a21d7de2fed143297810fe75ecc7d2d659aa Mon Sep 17 00:00:00 2001 From: Suyash Patil <127177049+suyashpatil78@users.noreply.github.com> Date: Tue, 10 Oct 2023 17:46:53 +0530 Subject: [PATCH 12/14] test: 40% coverage of my-view-advance-request page (#2495) * test: 40% coverage of my-view-advance-request page * minor * test: 100% coverage of my-view-adv-request page (#2496) * test: 100% coverage of my-view-adv-request page * minor --- .../core/mock-data/modal-controller.data.ts | 41 ++ .../services/advance-request.service.spec.ts | 4 +- .../core/services/advance-request.service.ts | 2 +- .../my-view-advance-request.page.spec.ts | 374 +++++++++++++++++- .../my-view-advance-request.page.ts | 2 +- 5 files changed, 405 insertions(+), 18 deletions(-) diff --git a/src/app/core/mock-data/modal-controller.data.ts b/src/app/core/mock-data/modal-controller.data.ts index e22e8ece25..407dda5402 100644 --- a/src/app/core/mock-data/modal-controller.data.ts +++ b/src/app/core/mock-data/modal-controller.data.ts @@ -513,3 +513,44 @@ export const modalControllerParams7 = { breakpoints: [0, 1], handle: false, }; + +export const popoverControllerParams8 = { + component: FyPopoverComponent, + componentProps: { + title: 'Pull Back Advance?', + formLabel: 'Pulling back your advance request will allow you to edit and re-submit the request.', + }, + cssClass: 'fy-dialog-popover', +}; + +export const modalControllerParams8 = { + component: ViewCommentComponent, + componentProps: { + objectType: 'advance_requests', + objectId: 'areqoVuT5I8OOy', + }, + cssClass: 'fy-modal', + showBackdrop: true, + canDismiss: true, + backdropDismiss: true, + animated: true, + initialBreakpoint: 1, + breakpoints: [0, 1], + handle: false, +}; + +export const modalControllerParams9 = { + component: FyViewAttachmentComponent, + componentProps: { + attachments: fileObject4[0], + }, + mode: 'ios' as Mode, + cssClass: 'fy-modal', + showBackdrop: true, + canDismiss: true, + backdropDismiss: true, + animated: true, + initialBreakpoint: 1, + breakpoints: [0, 1], + handle: false, +}; diff --git a/src/app/core/services/advance-request.service.spec.ts b/src/app/core/services/advance-request.service.spec.ts index f82e36c588..fed8fa019e 100644 --- a/src/app/core/services/advance-request.service.spec.ts +++ b/src/app/core/services/advance-request.service.spec.ts @@ -360,7 +360,7 @@ describe('AdvanceRequestService', () => { }); }); - it('pullBackadvanceRequest(): should pull back an advance requests', (done) => { + it('pullBackAdvanceRequest(): should pull back an advance requests', (done) => { apiService.post.and.returnValue(of(pullBackAdvancedRequests)); const payloadParam = { @@ -372,7 +372,7 @@ describe('AdvanceRequestService', () => { const advanceID = 'areqMP09oaYXBf'; - advanceRequestService.pullBackadvanceRequest(advanceID, payloadParam).subscribe((res) => { + advanceRequestService.pullBackAdvanceRequest(advanceID, payloadParam).subscribe((res) => { expect(res).toEqual(pullBackAdvancedRequests); expect(apiService.post).toHaveBeenCalledOnceWith(`/advance_requests/${advanceID}/pull_back`, payloadParam); done(); diff --git a/src/app/core/services/advance-request.service.ts b/src/app/core/services/advance-request.service.ts index 89bbcde095..6bf5413c43 100644 --- a/src/app/core/services/advance-request.service.ts +++ b/src/app/core/services/advance-request.service.ts @@ -121,7 +121,7 @@ export class AdvanceRequestService { @CacheBuster({ cacheBusterNotifier: advanceRequestsCacheBuster$, }) - pullBackadvanceRequest(advanceRequestId: string, addStatusPayload: StatusPayload): Observable { + pullBackAdvanceRequest(advanceRequestId: string, addStatusPayload: StatusPayload): Observable { return this.apiService.post('/advance_requests/' + advanceRequestId + '/pull_back', addStatusPayload); } diff --git a/src/app/fyle/my-view-advance-request/my-view-advance-request.page.spec.ts b/src/app/fyle/my-view-advance-request/my-view-advance-request.page.spec.ts index f0449f4690..a55098f9ff 100644 --- a/src/app/fyle/my-view-advance-request/my-view-advance-request.page.spec.ts +++ b/src/app/fyle/my-view-advance-request/my-view-advance-request.page.spec.ts @@ -1,26 +1,372 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { IonicModule } from '@ionic/angular'; +import { ComponentFixture, TestBed, fakeAsync, tick, waitForAsync } from '@angular/core/testing'; +import { IonicModule, ModalController, NavController, PopoverController } from '@ionic/angular'; import { MyViewAdvanceRequestPage } from './my-view-advance-request.page'; +import { LoaderService } from 'src/app/core/services/loader.service'; +import { AdvanceRequestService } from 'src/app/core/services/advance-request.service'; +import { FileService } from 'src/app/core/services/file.service'; +import { ActivatedRoute, Router, UrlSerializer } from '@angular/router'; +import { AdvanceRequestsCustomFieldsService } from 'src/app/core/services/advance-requests-custom-fields.service'; +import { ModalPropertiesService } from 'src/app/core/services/modal-properties.service'; +import { TrackingService } from 'src/app/core/services/tracking.service'; +import { ExpenseFieldsService } from 'src/app/core/services/expense-fields.service'; +import { MIN_SCREEN_WIDTH } from 'src/app/app.module'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { StatisticTypes } from 'src/app/shared/components/fy-statistic/statistic-type.enum'; +import { cloneDeep } from 'lodash'; +import { + advanceRequestFileUrlData, + advanceRequestFileUrlData2, + expectedFileData1, + fileObject10, + fileObject4, +} from 'src/app/core/mock-data/file-object.data'; +import { of } from 'rxjs'; +import { transformedResponse2 } from 'src/app/core/mock-data/expense-field.data'; +import { extendedAdvReqDraft } from 'src/app/core/mock-data/extended-advance-request.data'; +import { apiAdvanceRequestAction } from 'src/app/core/mock-data/advance-request-actions.data'; +import { advanceReqApprovals } from 'src/app/core/mock-data/approval.data'; +import { advanceRequestCustomFieldData2 } from 'src/app/core/mock-data/advance-requests-custom-fields.data'; +import { customFields } from 'src/app/core/mock-data/custom-field.data'; +import { advanceRequests } from 'src/app/core/mock-data/advance-requests.data'; +import { FyDeleteDialogComponent } from 'src/app/shared/components/fy-delete-dialog/fy-delete-dialog.component'; +import { properties } from 'src/app/core/mock-data/modal-properties.data'; +import { modalControllerParams8, modalControllerParams9 } from 'src/app/core/mock-data/modal-controller.data'; -xdescribe('MyViewAdvanceRequestPage', () => { +describe('MyViewAdvanceRequestPage', () => { let component: MyViewAdvanceRequestPage; let fixture: ComponentFixture; + let advanceRequestService: jasmine.SpyObj; + let fileService: jasmine.SpyObj; + let router: jasmine.SpyObj; + let popoverController: jasmine.SpyObj; + let loaderService: jasmine.SpyObj; + let advanceRequestsCustomFieldsService: jasmine.SpyObj; + let modalController: jasmine.SpyObj; + let modalProperties: jasmine.SpyObj; + let trackingService: jasmine.SpyObj; + let activatedRoute: jasmine.SpyObj; + let expenseFieldsService: jasmine.SpyObj; - beforeEach( - waitForAsync(() => { - TestBed.configureTestingModule({ - declarations: [MyViewAdvanceRequestPage], - imports: [IonicModule.forRoot()], - }).compileComponents(); + beforeEach(waitForAsync(() => { + const advanceRequestServiceSpy = jasmine.createSpyObj('AdvanceRequestService', [ + 'getAdvanceRequest', + 'getActions', + 'getInternalStateAndDisplayName', + 'getActiveApproversByAdvanceRequestId', + 'modifyAdvanceRequestCustomFields', + 'pullBackAdvanceRequest', + 'delete', + ]); + const fileServiceSpy = jasmine.createSpyObj('FileService', ['findByAdvanceRequestId', 'downloadUrl']); + const routerSpy = jasmine.createSpyObj('Router', ['navigate']); + const popoverControllerSpy = jasmine.createSpyObj('PopoverController', ['create']); + const modalControllerSpy = jasmine.createSpyObj('ModalController', ['create', 'getTop']); + const modalPropertiesSpy = jasmine.createSpyObj('ModalPropertiesService', ['getModalDefaultProperties']); + const advanceRequestsCustomFieldsServiceSpy = jasmine.createSpyObj('AdvanceRequestsCustomFieldsService', [ + 'getAll', + ]); + const trackingServiceSpy = jasmine.createSpyObj('TrackingService', ['addComment', 'viewComment']); + const expenseFieldsServiceSpy = jasmine.createSpyObj('ExpenseFieldsService', ['getAllEnabled']); + const navControllerSpy = jasmine.createSpyObj('NavController', ['navigateForward']); + const loaderServiceSpy = jasmine.createSpyObj('LoaderService', ['showLoader', 'hideLoader']); - fixture = TestBed.createComponent(MyViewAdvanceRequestPage); - component = fixture.componentInstance; - fixture.detectChanges(); - }) - ); + TestBed.configureTestingModule({ + declarations: [MyViewAdvanceRequestPage], + imports: [IonicModule.forRoot()], + providers: [ + { provide: AdvanceRequestService, useValue: advanceRequestServiceSpy }, + { provide: FileService, useValue: fileServiceSpy }, + { provide: Router, useValue: routerSpy }, + { provide: PopoverController, useValue: popoverControllerSpy }, + { provide: LoaderService, useValue: loaderServiceSpy }, + { provide: AdvanceRequestsCustomFieldsService, useValue: advanceRequestsCustomFieldsServiceSpy }, + { provide: ModalController, useValue: modalControllerSpy }, + { provide: ModalPropertiesService, useValue: modalPropertiesSpy }, + { provide: TrackingService, useValue: trackingServiceSpy }, + { + provide: ActivatedRoute, + useValue: { + snapshot: { + params: { + id: 'areqR1cyLgXdND', + }, + }, + }, + }, + { provide: ExpenseFieldsService, useValue: expenseFieldsServiceSpy }, + { + provide: MIN_SCREEN_WIDTH, + useValue: 230, + }, + UrlSerializer, + { provide: NavController, useValue: navControllerSpy }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(MyViewAdvanceRequestPage); + component = fixture.componentInstance; + advanceRequestService = TestBed.inject(AdvanceRequestService) as jasmine.SpyObj; + fileService = TestBed.inject(FileService) as jasmine.SpyObj; + router = TestBed.inject(Router) as jasmine.SpyObj; + popoverController = TestBed.inject(PopoverController) as jasmine.SpyObj; + loaderService = TestBed.inject(LoaderService) as jasmine.SpyObj; + advanceRequestsCustomFieldsService = TestBed.inject( + AdvanceRequestsCustomFieldsService + ) as jasmine.SpyObj; + modalController = TestBed.inject(ModalController) as jasmine.SpyObj; + modalProperties = TestBed.inject(ModalPropertiesService) as jasmine.SpyObj; + trackingService = TestBed.inject(TrackingService) as jasmine.SpyObj; + activatedRoute = TestBed.inject(ActivatedRoute) as jasmine.SpyObj; + expenseFieldsService = TestBed.inject(ExpenseFieldsService) as jasmine.SpyObj; + + fixture.detectChanges(); + })); it('should create', () => { expect(component).toBeTruthy(); }); + + it('statisticTypes(): should return statistic types', () => { + expect(component.StatisticTypes).toEqual(StatisticTypes); + }); + + it('getReceiptExtension(): should return the extension of the receipt', () => { + const mockFileObject = cloneDeep(advanceRequestFileUrlData2[0]); + const result = component.getReceiptExtension(mockFileObject.name); + expect(result).toEqual('pdf'); + }); + + describe('getReceiptDetails():', () => { + it('should return the receipt details with thumbnail as fy-receipt.svg if extension is pdf', () => { + spyOn(component, 'getReceiptExtension').and.returnValue('pdf'); + const mockFileObject = cloneDeep(advanceRequestFileUrlData2[0]); + const result = component.getReceiptDetails(mockFileObject); + expect(component.getReceiptExtension).toHaveBeenCalledOnceWith(mockFileObject.name); + expect(result).toEqual({ + type: 'pdf', + thumbnail: 'img/fy-pdf.svg', + }); + }); + + it('should return the receipt details with type as image and thumbnail as file url if extension is png', () => { + spyOn(component, 'getReceiptExtension').and.returnValue('png'); + const mockFileObject = cloneDeep(advanceRequestFileUrlData2[0]); + const result = component.getReceiptDetails(mockFileObject); + expect(component.getReceiptExtension).toHaveBeenCalledOnceWith(mockFileObject.name); + expect(result).toEqual({ + type: 'image', + thumbnail: + 'https://fyle-storage-mumbai-3.s3.amazonaws.com/2023-02-23/orrjqbDbeP9p/receipts/fiSSsy2Bf4Se.000.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20230223T151537Z&X-Amz-SignedHeaders=host&X-Amz-Expires=604800&X-Amz-Credential=AKIA54Z3LIXTX6CFH4VG%2F20230223%2Fap-south-1%2Fs3%2Faws4_request&X-Amz-Signature=d79c2711892e7cb3f072e223b7b416408c252da38e7df0995e3d256cd8509fee', + }); + }); + }); + + it('getAndUpdateProjectName(): should set project field name equal to field name having column name as project_id', () => { + expenseFieldsService.getAllEnabled.and.returnValue(of(transformedResponse2)); + + component.getAndUpdateProjectName(); + + expect(component.projectFieldName).toEqual('Purpose'); + }); + + describe('ionViewWillEnter():', () => { + beforeEach(() => { + loaderService.showLoader.and.resolveTo(); + loaderService.hideLoader.and.resolveTo(); + advanceRequestService.getAdvanceRequest.and.returnValue(of(extendedAdvReqDraft)); + advanceRequestService.getInternalStateAndDisplayName.and.returnValue({ + state: 'DRAFT', + name: 'Draft', + }); + advanceRequestService.getActions.and.returnValue(of(apiAdvanceRequestAction)); + advanceRequestService.getActiveApproversByAdvanceRequestId.and.returnValue(of(advanceReqApprovals)); + const mockFileObject = cloneDeep(advanceRequestFileUrlData[0]); + spyOn(component, 'getReceiptDetails').and.returnValue({ + type: 'pdf', + thumbnail: 'src/assets/images/pdf-receipt-placeholder.png', + }); + fileService.downloadUrl.and.returnValue(of('mockdownloadurl.png')); + fileService.findByAdvanceRequestId.and.returnValue(of([mockFileObject])); + const mockAdvRequestCustomFields = cloneDeep(advanceRequestCustomFieldData2); + advanceRequestsCustomFieldsService.getAll.and.returnValue(of(mockAdvRequestCustomFields)); + spyOn(component, 'getAndUpdateProjectName'); + advanceRequestService.modifyAdvanceRequestCustomFields.and.returnValue(customFields); + }); + + it('should set advanceRequest$, internal state and currency symbol to $', fakeAsync(() => { + component.ionViewWillEnter(); + tick(100); + + component.advanceRequest$.subscribe((res) => { + expect(advanceRequestService.getAdvanceRequest).toHaveBeenCalledOnceWith('areqR1cyLgXdND'); + expect(loaderService.showLoader).toHaveBeenCalledTimes(1); + expect(loaderService.hideLoader).toHaveBeenCalledTimes(1); + expect(res).toEqual(extendedAdvReqDraft); + }); + expect(advanceRequestService.getInternalStateAndDisplayName).toHaveBeenCalledOnceWith(extendedAdvReqDraft); + expect(component.internalState).toEqual({ + state: 'DRAFT', + name: 'Draft', + }); + expect(component.currencySymbol).toEqual('$'); + })); + + it('should set currency symbol to undefined if advance request is undefined', fakeAsync(() => { + advanceRequestService.getAdvanceRequest.and.returnValue(of(undefined)); + component.ionViewWillEnter(); + tick(100); + + expect(component.internalState).toEqual({ + state: 'DRAFT', + name: 'Draft', + }); + expect(component.currencySymbol).toEqual(undefined); + })); + + it('should set actions$ to actions', fakeAsync(() => { + component.ionViewWillEnter(); + tick(100); + + component.actions$.subscribe((res) => { + expect(res).toEqual(apiAdvanceRequestAction); + }); + expect(advanceRequestService.getActions).toHaveBeenCalledOnceWith('areqR1cyLgXdND'); + })); + + it('should set activeApprovals$ to active approvals', fakeAsync(() => { + component.ionViewWillEnter(); + tick(100); + + component.activeApprovals$.subscribe((res) => { + expect(res).toEqual(advanceReqApprovals); + }); + expect(advanceRequestService.getActiveApproversByAdvanceRequestId).toHaveBeenCalledOnceWith('areqR1cyLgXdND'); + })); + + it('should set attachedFiles$ to attached files', fakeAsync(() => { + const mockFileObject = cloneDeep(expectedFileData1[0]); + fileService.findByAdvanceRequestId.and.returnValue(of([mockFileObject])); + component.ionViewWillEnter(); + tick(100); + + component.attachedFiles$.subscribe((res) => { + expect(fileService.findByAdvanceRequestId).toHaveBeenCalledOnceWith('areqR1cyLgXdND'); + expect(fileService.downloadUrl).toHaveBeenCalledOnceWith('fiV1gXpyCcbU'); + expect(res).toEqual([mockFileObject]); + }); + })); + + it('should call advanceRequestService.modifyAdvanceRequestCustomFields and getAndUpdateProjectName once', fakeAsync(() => { + const mockAdvRequestCustomFields = cloneDeep(advanceRequestCustomFieldData2); + advanceRequestsCustomFieldsService.getAll.and.returnValue(of(mockAdvRequestCustomFields)); + + component.ionViewWillEnter(); + tick(100); + + component.advanceRequestCustomFields$.subscribe(() => { + expect(advanceRequestService.modifyAdvanceRequestCustomFields).toHaveBeenCalledOnceWith( + JSON.parse(extendedAdvReqDraft.areq_custom_field_values) + ); + expect(advanceRequestsCustomFieldsService.getAll).toHaveBeenCalledTimes(1); + }); + expect(component.getAndUpdateProjectName).toHaveBeenCalledTimes(1); + })); + }); + + it('pullBack(): should pull back advance request and navigate to my_advances page', fakeAsync(() => { + advanceRequestService.pullBackAdvanceRequest.and.returnValue(of(advanceRequests)); + loaderService.showLoader.and.resolveTo(); + loaderService.hideLoader.and.resolveTo(); + const pullBackPopoverSpy = jasmine.createSpyObj('pullBackPopover', ['present', 'onWillDismiss']); + pullBackPopoverSpy.onWillDismiss.and.resolveTo({ data: { comment: 'test comment' } }); + popoverController.create.and.resolveTo(pullBackPopoverSpy); + component.pullBack(); + tick(100); + + expect(advanceRequestService.pullBackAdvanceRequest).toHaveBeenCalledOnceWith('areqR1cyLgXdND', { + status: { + comment: 'test comment', + }, + notify: false, + }); + expect(router.navigate).toHaveBeenCalledOnceWith(['/', 'enterprise', 'my_advances']); + expect(loaderService.showLoader).toHaveBeenCalledTimes(1); + expect(loaderService.hideLoader).toHaveBeenCalledTimes(1); + })); + + it('edit(): should navigate to add-edit-advance-request page', () => { + component.edit(); + expect(router.navigate).toHaveBeenCalledOnceWith([ + '/', + 'enterprise', + 'add_edit_advance_request', + { id: 'areqR1cyLgXdND' }, + ]); + }); + + it('delete(): should show delete advance request popover and navigate to my_advances page', fakeAsync(() => { + const deletePopoverSpy = jasmine.createSpyObj('deletePopover', ['present', 'onDidDismiss']); + deletePopoverSpy.onDidDismiss.and.resolveTo({ data: { status: 'success' } }); + popoverController.create.and.resolveTo(deletePopoverSpy); + advanceRequestService.delete.and.returnValue(of(advanceRequests)); + + component.delete(); + tick(100); + + expect(deletePopoverSpy.present).toHaveBeenCalledTimes(1); + expect(deletePopoverSpy.onDidDismiss).toHaveBeenCalledTimes(1); + expect(router.navigate).toHaveBeenCalledOnceWith(['/', 'enterprise', 'my_advances']); + })); + + describe('openCommentsModal():', () => { + let commentsModalSpy: jasmine.SpyObj; + beforeEach(() => { + component.advanceRequest$ = of(extendedAdvReqDraft); + modalProperties.getModalDefaultProperties.and.returnValue(properties); + commentsModalSpy = jasmine.createSpyObj('modal', ['present', 'onDidDismiss']); + commentsModalSpy.onDidDismiss.and.resolveTo({ data: { updated: true } }); + modalController.create.and.resolveTo(commentsModalSpy); + }); + + it('should open comments modal and track addComment event if updated is true', fakeAsync(() => { + component.openCommentsModal(); + tick(100); + + expect(modalController.create).toHaveBeenCalledOnceWith(modalControllerParams8); + expect(modalProperties.getModalDefaultProperties).toHaveBeenCalledTimes(1); + expect(commentsModalSpy.present).toHaveBeenCalledTimes(1); + expect(commentsModalSpy.onDidDismiss).toHaveBeenCalledTimes(1); + expect(trackingService.addComment).toHaveBeenCalledTimes(1); + expect(trackingService.viewComment).not.toHaveBeenCalled(); + })); + + it('should open comments modal and track viewComment event if updated is false', fakeAsync(() => { + commentsModalSpy.onDidDismiss.and.resolveTo({ data: { updated: false } }); + modalController.create.and.resolveTo(commentsModalSpy); + + component.openCommentsModal(); + tick(100); + + expect(modalController.create).toHaveBeenCalledOnceWith(modalControllerParams8); + expect(modalProperties.getModalDefaultProperties).toHaveBeenCalledTimes(1); + expect(commentsModalSpy.present).toHaveBeenCalledTimes(1); + expect(commentsModalSpy.onDidDismiss).toHaveBeenCalledTimes(1); + expect(trackingService.viewComment).toHaveBeenCalledTimes(1); + expect(trackingService.addComment).not.toHaveBeenCalled(); + })); + }); + + it('viewAttachments(): should open attachments modal', fakeAsync(() => { + const attachmentsModalSpy = jasmine.createSpyObj('attachmentsModal', ['present']); + modalController.create.and.resolveTo(attachmentsModalSpy); + modalProperties.getModalDefaultProperties.and.returnValue(properties); + component.viewAttachments(fileObject4[0]); + tick(100); + + expect(modalController.create).toHaveBeenCalledOnceWith(modalControllerParams9); + expect(attachmentsModalSpy.present).toHaveBeenCalledTimes(1); + expect(modalProperties.getModalDefaultProperties).toHaveBeenCalledTimes(1); + })); }); diff --git a/src/app/fyle/my-view-advance-request/my-view-advance-request.page.ts b/src/app/fyle/my-view-advance-request/my-view-advance-request.page.ts index 2b59220f9c..0d4bb4d649 100644 --- a/src/app/fyle/my-view-advance-request/my-view-advance-request.page.ts +++ b/src/app/fyle/my-view-advance-request/my-view-advance-request.page.ts @@ -199,7 +199,7 @@ export class MyViewAdvanceRequestPage implements OnInit { from(this.loaderService.showLoader()) .pipe( - switchMap(() => this.advanceRequestService.pullBackadvanceRequest(id, statusPayload)), + switchMap(() => this.advanceRequestService.pullBackAdvanceRequest(id, statusPayload)), finalize(() => from(this.loaderService.hideLoader())) ) .subscribe(() => { From e275965f216ada3e5808a755e48c7e260a446746 Mon Sep 17 00:00:00 2001 From: Suyash Patil <127177049+suyashpatil78@users.noreply.github.com> Date: Tue, 10 Oct 2023 17:58:09 +0530 Subject: [PATCH 13/14] 100% coverage of my view advances page (#2498) --- .../my-view-advance.page.spec.ts | 116 +++++++++++++++--- .../my-view-advance/my-view-advance.page.ts | 17 ++- 2 files changed, 110 insertions(+), 23 deletions(-) diff --git a/src/app/fyle/my-view-advance/my-view-advance.page.spec.ts b/src/app/fyle/my-view-advance/my-view-advance.page.spec.ts index 0b3bd4aadb..d129b23fd6 100644 --- a/src/app/fyle/my-view-advance/my-view-advance.page.spec.ts +++ b/src/app/fyle/my-view-advance/my-view-advance.page.spec.ts @@ -1,26 +1,114 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { IonicModule } from '@ionic/angular'; +import { ComponentFixture, TestBed, fakeAsync, tick, waitForAsync } from '@angular/core/testing'; +import { IonicModule, NavController } from '@ionic/angular'; import { MyViewAdvancePage } from './my-view-advance.page'; +import { AdvanceService } from 'src/app/core/services/advance.service'; +import { ActivatedRoute, Router, UrlSerializer } from '@angular/router'; +import { LoaderService } from 'src/app/core/services/loader.service'; +import { ExpenseFieldsService } from 'src/app/core/services/expense-fields.service'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { StatisticTypes } from 'src/app/shared/components/fy-statistic/statistic-type.enum'; +import { transformedResponse2 } from 'src/app/core/mock-data/expense-field.data'; +import { of } from 'rxjs'; +import { singleExtendedAdvancesData3 } from 'src/app/core/mock-data/extended-advance.data'; -xdescribe('MyViewAdvancePage', () => { +describe('MyViewAdvancePage', () => { let component: MyViewAdvancePage; let fixture: ComponentFixture; + let advanceService: jasmine.SpyObj; + let activatedRoute: jasmine.SpyObj; + let loaderService: jasmine.SpyObj; + let expenseFieldsService: jasmine.SpyObj; - beforeEach( - waitForAsync(() => { - TestBed.configureTestingModule({ - declarations: [MyViewAdvancePage], - imports: [IonicModule.forRoot()], - }).compileComponents(); + beforeEach(waitForAsync(() => { + const advanceServiceSpy = jasmine.createSpyObj('AdvanceService', ['getAdvance']); + const loaderServiceSpy = jasmine.createSpyObj('LoaderService', ['showLoader', 'hideLoader']); + const expenseFieldsServiceSpy = jasmine.createSpyObj('ExpenseFieldsService', ['getAllEnabled']); + const routerSpy = jasmine.createSpyObj('Router', ['navigate']); + const navControllerSpy = jasmine.createSpyObj('NavController', ['navigateForward']); - fixture = TestBed.createComponent(MyViewAdvancePage); - component = fixture.componentInstance; - fixture.detectChanges(); - }) - ); + TestBed.configureTestingModule({ + declarations: [MyViewAdvancePage], + imports: [IonicModule.forRoot()], + providers: [ + { provide: AdvanceService, useValue: advanceServiceSpy }, + { + provide: ActivatedRoute, + useValue: { + snapshot: { + params: { + id: 'advETmi3eePvQ', + }, + }, + }, + }, + { provide: LoaderService, useValue: loaderServiceSpy }, + { provide: ExpenseFieldsService, useValue: expenseFieldsServiceSpy }, + UrlSerializer, + { + provide: NavController, + useValue: navControllerSpy, + }, + { provide: Router, useValue: routerSpy }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(MyViewAdvancePage); + component = fixture.componentInstance; + advanceService = TestBed.inject(AdvanceService) as jasmine.SpyObj; + activatedRoute = TestBed.inject(ActivatedRoute) as jasmine.SpyObj; + loaderService = TestBed.inject(LoaderService) as jasmine.SpyObj; + expenseFieldsService = TestBed.inject(ExpenseFieldsService) as jasmine.SpyObj; + fixture.detectChanges(); + })); it('should create', () => { expect(component).toBeTruthy(); }); + + it('StatisticTypes(): should return StatisticTypes', () => { + expect(component.StatisticTypes).toEqual(StatisticTypes); + }); + + it('getAndUpdateProjectName(): should set project field name equal to field name having column name as project_id', () => { + expenseFieldsService.getAllEnabled.and.returnValue(of(transformedResponse2)); + + component.getAndUpdateProjectName(); + + expect(component.projectFieldName).toEqual('Purpose'); + }); + + describe('ionViewWillEnter()', () => { + beforeEach(() => { + spyOn(component, 'getAndUpdateProjectName'); + loaderService.showLoader.and.resolveTo(); + loaderService.hideLoader.and.resolveTo(); + advanceService.getAdvance.and.returnValue(of(singleExtendedAdvancesData3)); + }); + + it('should set advance$ correctly', fakeAsync(() => { + component.ionViewWillEnter(); + tick(100); + + expect(loaderService.showLoader).toHaveBeenCalledTimes(1); + expect(loaderService.hideLoader).toHaveBeenCalledTimes(1); + expect(advanceService.getAdvance).toHaveBeenCalledOnceWith('advETmi3eePvQ'); + })); + + it('should set currencySymbol to ₹ if advance currency is defined', fakeAsync(() => { + component.ionViewWillEnter(); + tick(100); + + expect(component.currencySymbol).toEqual('₹'); + })); + + it('should set currencySymbol to undefined if advance is undefined', fakeAsync(() => { + advanceService.getAdvance.and.returnValue(of(undefined)); + component.ionViewWillEnter(); + tick(100); + + expect(component.currencySymbol).toEqual(undefined); + })); + }); }); diff --git a/src/app/fyle/my-view-advance/my-view-advance.page.ts b/src/app/fyle/my-view-advance/my-view-advance.page.ts index 9ead1f1fae..892836049a 100644 --- a/src/app/fyle/my-view-advance/my-view-advance.page.ts +++ b/src/app/fyle/my-view-advance/my-view-advance.page.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { from, Observable } from 'rxjs'; import { finalize, shareReplay, switchMap } from 'rxjs/operators'; @@ -7,14 +7,15 @@ import { LoaderService } from 'src/app/core/services/loader.service'; import { StatisticTypes } from 'src/app/shared/components/fy-statistic/statistic-type.enum'; import { getCurrencySymbol } from '@angular/common'; import { ExpenseFieldsService } from 'src/app/core/services/expense-fields.service'; +import { ExtendedAdvance } from 'src/app/core/models/extended_advance.model'; @Component({ selector: 'app-my-view-advance', templateUrl: './my-view-advance.page.html', styleUrls: ['./my-view-advance.page.scss'], }) -export class MyViewAdvancePage implements OnInit { - advance$: Observable; +export class MyViewAdvancePage { + advance$: Observable; projectFieldName = 'Project'; @@ -27,12 +28,12 @@ export class MyViewAdvancePage implements OnInit { private expenseFieldsService: ExpenseFieldsService ) {} - get StatisticTypes() { + get StatisticTypes(): typeof StatisticTypes { return StatisticTypes; } // TODO replace forEach with find - getAndUpdateProjectName() { + getAndUpdateProjectName(): void { this.expenseFieldsService.getAllEnabled().subscribe((expenseFields) => { expenseFields.forEach((expenseField) => { if (expenseField.column_name === 'project_id') { @@ -42,8 +43,8 @@ export class MyViewAdvancePage implements OnInit { }); } - ionViewWillEnter() { - const id = this.activatedRoute.snapshot.params.id; + ionViewWillEnter(): void { + const id = this.activatedRoute.snapshot.params.id as string; this.advance$ = from(this.loaderService.showLoader()).pipe( switchMap(() => this.advanceService.getAdvance(id)), @@ -57,6 +58,4 @@ export class MyViewAdvancePage implements OnInit { this.getAndUpdateProjectName(); } - - ngOnInit() {} } From 4e75133f17232b9f5acce413742fbe8f85e9f8a6 Mon Sep 17 00:00:00 2001 From: jayfyle <115472256+jayfyle@users.noreply.github.com> Date: Wed, 11 Oct 2023 11:18:28 +0530 Subject: [PATCH 14/14] feat: Removing Push Notifications in Settings (#2475) * removed push notifications * removed notification column --------- Co-authored-by: Jay Budhadev --- src/app/fyle/notifications/notifications.page.html | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/app/fyle/notifications/notifications.page.html b/src/app/fyle/notifications/notifications.page.html index cc8380601d..08f718de0d 100644 --- a/src/app/fyle/notifications/notifications.page.html +++ b/src/app/fyle/notifications/notifications.page.html @@ -31,13 +31,6 @@

Delegatee Notification

{{ feature.value.textLabel }}
- - - - Delegatee Notification
{{ event.textLabel }}
- -
- -
-
+