Skip to content

Commit

Permalink
feat: Mileage/ Per Diem expenses creation to depend on category (#2461)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
arjunaj5 authored Oct 4, 2023
1 parent 273febf commit a63aa8f
Show file tree
Hide file tree
Showing 13 changed files with 265 additions and 36 deletions.
42 changes: 42 additions & 0 deletions src/app/core/mock-data/action-sheet-options.data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
];
4 changes: 4 additions & 0 deletions src/app/core/mock-data/allowed-expense-types.data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const allowedExpenseTypes: Record<string, boolean> = {
mileage: true,
perDiem: true,
};
28 changes: 28 additions & 0 deletions src/app/core/mock-data/org-category.data.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { PlatformCategory } from '../models/platform/platform-category.model';
import { OrgCategory } from '../models/v1/org-category.model';

export const orgCategoryData: OrgCategory = {
Expand All @@ -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',
Expand Down
17 changes: 17 additions & 0 deletions src/app/core/services/categories.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
13 changes: 13 additions & 0 deletions src/app/core/services/categories.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,19 @@ export class CategoriesService {
);
}

@Cacheable()
getMileageOrPerDiemCategories(): Observable<PlatformCategory[]> {
const data = {
params: {
is_enabled: 'eq.true',
system_category: 'in.(Mileage, Per Diem)',
},
};
return this.spenderPlatformV1ApiService
.get<PlatformApiResponse<PlatformCategory>>('/categories', data)
.pipe(map((res) => res.data));
}

transformFrom(platformCategory: PlatformCategory[]): OrgCategory[] {
const oldCategory = platformCategory.map((category) => ({
code: category.code,
Expand Down
13 changes: 10 additions & 3 deletions src/app/fyle/dashboard/dashboard.page.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,16 @@
</ion-buttons>
<ion-title class="stats--title" mode="md"> Home </ion-title>
<ion-buttons slot="end" *ngIf="isConnected$ | async">
<div class="stats--action-shortcut" (click)="openAddExpenseActionSheet()">
<mat-icon svgIcon="fy-plus"></mat-icon>
</div>
<ng-container *ngIf="(orgSettings$ | async) && (specialCategories$ | async); else actionLoader">
<div class="stats--action-shortcut" (click)="openAddExpenseActionSheet()">
<mat-icon svgIcon="fy-plus"></mat-icon>
</div>
</ng-container>
<ng-template #actionLoader>
<div class="stats--action-shortcut--skeleton-icon">
<ion-skeleton-text class="m-0" animated></ion-skeleton-text>
</div>
</ng-template>
</ion-buttons>
</ion-toolbar>
</ion-header>
Expand Down
7 changes: 7 additions & 0 deletions src/app/fyle/dashboard/dashboard.page.scss
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,12 @@
&--action-shortcut {
padding-right: 12px;
padding-top: 2px;

&--skeleton-icon {
margin-right: 12px;
width: 24px;
height: 24px;
border-radius: 5px;
}
}
}
45 changes: 38 additions & 7 deletions src/app/fyle/dashboard/dashboard.page.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -34,6 +41,7 @@ describe('DashboardPage', () => {
let smartlookService: jasmine.SpyObj<SmartlookService>;
let orgSettingsService: jasmine.SpyObj<OrgSettingsService>;
let orgUserSettingsService: jasmine.SpyObj<OrgUserSettingsService>;
let categoriesService: jasmine.SpyObj<CategoriesService>;
let backButtonService: jasmine.SpyObj<BackButtonService>;
let platform: Platform;
let navController: jasmine.SpyObj<NavController>;
Expand All @@ -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']);

Expand All @@ -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,
Expand Down Expand Up @@ -98,6 +108,7 @@ describe('DashboardPage', () => {
smartlookService = TestBed.inject(SmartlookService) as jasmine.SpyObj<SmartlookService>;
orgSettingsService = TestBed.inject(OrgSettingsService) as jasmine.SpyObj<OrgSettingsService>;
orgUserSettingsService = TestBed.inject(OrgUserSettingsService) as jasmine.SpyObj<OrgUserSettingsService>;
categoriesService = TestBed.inject(CategoriesService) as jasmine.SpyObj<CategoriesService>;
backButtonService = TestBed.inject(BackButtonService) as jasmine.SpyObj<BackButtonService>;
platform = TestBed.inject(Platform);
navController = TestBed.inject(NavController) as jasmine.SpyObj<NavController>;
Expand Down Expand Up @@ -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']);
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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(() => {
Expand Down
27 changes: 20 additions & 7 deletions src/app/fyle/dashboard/dashboard.page.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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,
Expand All @@ -41,6 +43,8 @@ export class DashboardPage {

orgSettings$: Observable<OrgSettings>;

specialCategories$: Observable<PlatformCategory[]>;

homeCurrency$: Observable<string>;

isConnected$: Observable<boolean>;
Expand All @@ -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
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -229,10 +242,10 @@ export class DashboardPage {
};
}

setupActionSheet(orgSettings: OrgSettings): void {
setupActionSheet(orgSettings: OrgSettings, allowedExpenseTypes: Record<string, boolean>): 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',
Expand All @@ -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',
Expand Down
15 changes: 12 additions & 3 deletions src/app/fyle/my-expenses/my-expenses.page.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,18 @@
<ion-icon slot="icon-only" src="../../../assets/svg/fy-unapplied-filter.svg"></ion-icon>
</ng-template>
</ion-button>
<ion-button *ngIf="isConnected$ | async" class="my-expenses--header-btn" (click)="openActionSheet()">
<ion-icon slot="icon-only" src="../../../assets/svg/fy-plus.svg"></ion-icon>
</ion-button>
<ng-container *ngIf="isConnected$ | async">
<ng-container *ngIf="(orgSettings$ | async) && (specialCategories$ | async); else actionLoader">
<ion-button class="my-expenses--header-btn" (click)="openActionSheet()">
<ion-icon slot="icon-only" src="../../../assets/svg/fy-plus.svg"></ion-icon>
</ion-button>
</ng-container>
<ng-template #actionLoader>
<div class="my-expenses--header-btn--skeleton-loader">
<ion-skeleton-text class="m-0" animated></ion-skeleton-text>
</div>
</ng-template>
</ng-container>
</ion-buttons>
</ng-container>

Expand Down
7 changes: 7 additions & 0 deletions src/app/fyle/my-expenses/my-expenses.page.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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%;
Expand Down
Loading

0 comments on commit a63aa8f

Please sign in to comment.