From 80aa5e4eaa59de072cc6851a0799106d814120c1 Mon Sep 17 00:00:00 2001 From: Chethan-Fyle <93918979+Chethan-Fyle@users.noreply.github.com> Date: Mon, 27 May 2024 12:10:00 +0530 Subject: [PATCH] feat: migrate to platform files api in view expense page (#2920) --- src/app/core/mock-data/platform-file.data.ts | 12 +++++ .../v1/attach-receipt-payload.data.ts | 17 +++++++ .../mock-data/platform/v1/expense.data.ts | 24 +++++++++ .../platform/v1/expenses-response.data.ts | 6 +++ .../platform/v1/approver/file.service.spec.ts | 29 +++++++++++ .../platform/v1/approver/file.service.ts | 2 +- .../v1/spender/expenses.service.spec.ts | 31 +++++++++++- .../platform/v1/spender/file.service.spec.ts | 29 +++++++++++ .../platform/v1/spender/file.service.ts | 2 +- .../view-expense/view-expense.page.spec.ts | 40 +++++++++++---- .../fyle/view-expense/view-expense.page.ts | 50 ++++++++++++------- .../fyle/view-mileage/view-mileage.page.ts | 4 +- 12 files changed, 215 insertions(+), 31 deletions(-) create mode 100644 src/app/core/mock-data/platform-file.data.ts create mode 100644 src/app/core/mock-data/platform/v1/attach-receipt-payload.data.ts diff --git a/src/app/core/mock-data/platform-file.data.ts b/src/app/core/mock-data/platform-file.data.ts new file mode 100644 index 0000000000..17677edfb6 --- /dev/null +++ b/src/app/core/mock-data/platform-file.data.ts @@ -0,0 +1,12 @@ +import deepFreeze from 'deep-freeze-strict'; +import { PlatformFileGenerateUrlsResponse } from '../models/platform/platform-file-generate-urls-response.model'; + +export const urlsBulkData: PlatformFileGenerateUrlsResponse[] = deepFreeze([ + { + content_type: 'application/pdf', + download_url: 'https://exampledownloadurl.com', + id: 'fij7umnwoZUU', + name: 'invoice.pdf', + upload_url: 'https://exampleuploadurl.com', + }, +]); diff --git a/src/app/core/mock-data/platform/v1/attach-receipt-payload.data.ts b/src/app/core/mock-data/platform/v1/attach-receipt-payload.data.ts new file mode 100644 index 0000000000..9ed4b3d72e --- /dev/null +++ b/src/app/core/mock-data/platform/v1/attach-receipt-payload.data.ts @@ -0,0 +1,17 @@ +import deepFreeze from 'deep-freeze-strict'; + +export const attachReceiptPayload1 = deepFreeze({ + data: { + id: 'txcSFe6efB6R', + file_id: 'fi1w2IE6JeqS', + }, +}); + +export const attachReceiptsPayload1 = deepFreeze({ + data: [ + { + id: 'txcSFe6efB6R', + file_ids: ['fi1w2IE6JeqS', 'fiQ8ejWk25rF'], + }, + ], +}); diff --git a/src/app/core/mock-data/platform/v1/expense.data.ts b/src/app/core/mock-data/platform/v1/expense.data.ts index 96922690ee..aeac6ed4ab 100644 --- a/src/app/core/mock-data/platform/v1/expense.data.ts +++ b/src/app/core/mock-data/platform/v1/expense.data.ts @@ -10,6 +10,7 @@ import { AccountType } from 'src/app/core/models/platform/v1/account.model'; import { Expense, TransactionStatus } from 'src/app/core/models/platform/v1/expense.model'; import { FileType } from 'src/app/core/models/platform/v1/file.model'; import { CustomFieldTypes } from 'src/app/core/enums/platform/v1/custom-fields-type.enum'; +import { CommuteDeduction } from 'src/app/core/enums/commute-deduction.enum'; export const expenseData: Expense = deepFreeze({ accounting_export_summary: {}, @@ -267,6 +268,29 @@ export const mileageExpense: Expense = deepFreeze({ category_id: 247012, claim_amount: 459, code: null, + commute_deduction: CommuteDeduction.ONE_WAY, + commute_details: { + distance: 2.92, + distance_unit: 'MILES', + home_location: { + city: 'Bengaluru', + country: 'India', + formatted_address: 'Bengaluru, Karnataka, India', + latitude: 13.0035068, + longitude: 77.5890953, + state: 'Karnataka', + }, + id: 96, + work_location: { + city: 'Bengaluru', + country: 'India', + formatted_address: 'Sarjapura, Bengaluru, Karnataka 562125, India', + latitude: 12.8575579, + longitude: 77.7864057, + state: 'Karnataka', + }, + }, + commute_details_id: 96, cost_center: { code: null, id: 2885, diff --git a/src/app/core/mock-data/platform/v1/expenses-response.data.ts b/src/app/core/mock-data/platform/v1/expenses-response.data.ts index 7d11e075af..31995d95c6 100644 --- a/src/app/core/mock-data/platform/v1/expenses-response.data.ts +++ b/src/app/core/mock-data/platform/v1/expenses-response.data.ts @@ -9,3 +9,9 @@ export const expensesResponse: PlatformApiResponse = deepFreeze({ data: [expenseData], offset: 0, }); + +export const expenseResponse: PlatformApiResponse = deepFreeze({ + count: 1, + data: expenseData, + offset: 0, +}); diff --git a/src/app/core/services/platform/v1/approver/file.service.spec.ts b/src/app/core/services/platform/v1/approver/file.service.spec.ts index 497dbcfe2d..6124659560 100644 --- a/src/app/core/services/platform/v1/approver/file.service.spec.ts +++ b/src/app/core/services/platform/v1/approver/file.service.spec.ts @@ -1,6 +1,8 @@ import { TestBed } from '@angular/core/testing'; import { ApproverFileService } from './file.service'; import { ApproverPlatformApiService } from '../../../approver-platform-api.service'; +import { generateUrlsBulkData1 } from 'src/app/core/mock-data/generate-urls-bulk-response.data'; +import { of } from 'rxjs'; describe('ApproverFileService', () => { let service: ApproverFileService; @@ -25,4 +27,31 @@ describe('ApproverFileService', () => { it('should be created', () => { expect(service).toBeTruthy(); }); + + it('generateUrls(): should generate upload and download urls for the given file', (done) => { + approverPlatformApiService.post.and.returnValue(of({ data: generateUrlsBulkData1[0] })); + + service.generateUrls('fi').subscribe((response) => { + expect(response).toEqual(generateUrlsBulkData1[0]); + done(); + }); + }); + + it('generateUrlsBulk(): should generate upload and download urls for multiple files', (done) => { + approverPlatformApiService.post.and.returnValue(of({ data: generateUrlsBulkData1 })); + + service.generateUrlsBulk(['fi']).subscribe((response) => { + expect(response).toEqual(generateUrlsBulkData1); + done(); + }); + }); + + it('downloadFile(): should download file', (done) => { + approverPlatformApiService.get.and.returnValue(of({})); + + service.downloadFile('fi').subscribe((response) => { + expect(response).toEqual({}); + done(); + }); + }); }); diff --git a/src/app/core/services/platform/v1/approver/file.service.ts b/src/app/core/services/platform/v1/approver/file.service.ts index 0c07ae4164..787acdc84a 100644 --- a/src/app/core/services/platform/v1/approver/file.service.ts +++ b/src/app/core/services/platform/v1/approver/file.service.ts @@ -42,7 +42,7 @@ export class ApproverFileService { .pipe(map((response) => response.data)); } - downloadFile(id: string): {} { + downloadFile(id: string): Observable<{}> { return this.approverPlatformApiService.get('/files/download?id=' + id); } } diff --git a/src/app/core/services/platform/v1/spender/expenses.service.spec.ts b/src/app/core/services/platform/v1/spender/expenses.service.spec.ts index 5a85936951..31d397eda8 100644 --- a/src/app/core/services/platform/v1/spender/expenses.service.spec.ts +++ b/src/app/core/services/platform/v1/spender/expenses.service.spec.ts @@ -4,16 +4,21 @@ import { ExpensesService } from './expenses.service'; import { SpenderService } from './spender.service'; import { expenseData, + expenseResponseData, readyToReportExpensesData2, splitExpensesData, } from 'src/app/core/mock-data/platform/v1/expense.data'; import { PAGINATION_SIZE } from 'src/app/constants'; -import { expensesResponse } from 'src/app/core/mock-data/platform/v1/expenses-response.data'; +import { expenseResponse, expensesResponse } from 'src/app/core/mock-data/platform/v1/expenses-response.data'; import { getExpensesQueryParams } from 'src/app/core/mock-data/platform/v1/expenses-query-params.data'; import { expenseDuplicateSets } from 'src/app/core/mock-data/platform/v1/expense-duplicate-sets.data'; import { completeStats } from 'src/app/core/mock-data/platform/v1/expenses-stats.data'; import { ExpensesService as SharedExpenseService } from '../shared/expenses.service'; import { expensesCacheBuster$ } from 'src/app/core/cache-buster/expense-cache-buster'; +import { + attachReceiptPayload1, + attachReceiptsPayload1, +} from 'src/app/core/mock-data/platform/v1/attach-receipt-payload.data'; describe('ExpensesService', () => { let service: ExpensesService; @@ -238,4 +243,28 @@ describe('ExpensesService', () => { done(); }); }); + + it('attachReceiptToExpense(): should attach a receipt to an expense', (done) => { + spenderService.post.and.returnValue(of(expenseResponse)); + + service + .attachReceiptToExpense(attachReceiptPayload1.data.id, attachReceiptPayload1.data.file_id) + .subscribe((res) => { + expect(res).toEqual(expenseData); + expect(spenderService.post).toHaveBeenCalledOnceWith('/expenses/attach_receipt', attachReceiptPayload1); + done(); + }); + }); + + it('attachReceiptsToExpense(): should attach multiple receipts to an expense', (done) => { + spenderService.post.and.returnValue(of(expensesResponse)); + + service + .attachReceiptsToExpense(attachReceiptPayload1.data.id, attachReceiptsPayload1.data[0].file_ids) + .subscribe((res) => { + expect(res).toEqual(expensesResponse.data); + expect(spenderService.post).toHaveBeenCalledOnceWith('/expenses/attach_files/bulk', attachReceiptsPayload1); + done(); + }); + }); }); diff --git a/src/app/core/services/platform/v1/spender/file.service.spec.ts b/src/app/core/services/platform/v1/spender/file.service.spec.ts index 7ea52382b3..2d01ef2fbd 100644 --- a/src/app/core/services/platform/v1/spender/file.service.spec.ts +++ b/src/app/core/services/platform/v1/spender/file.service.spec.ts @@ -1,6 +1,8 @@ import { TestBed } from '@angular/core/testing'; import { SpenderFileService } from './file.service'; import { SpenderPlatformV1ApiService } from '../../../spender-platform-v1-api.service'; +import { of } from 'rxjs'; +import { generateUrlsBulkData1 } from 'src/app/core/mock-data/generate-urls-bulk-response.data'; describe('SpenderFileService', () => { let service: SpenderFileService; @@ -25,4 +27,31 @@ describe('SpenderFileService', () => { it('should be created', () => { expect(service).toBeTruthy(); }); + + it('generateUrls(): should generate upload and download urls for the given file', (done) => { + spenderPlatformV1ApiService.post.and.returnValue(of({ data: generateUrlsBulkData1[0] })); + + service.generateUrls('fi').subscribe((response) => { + expect(response).toEqual(generateUrlsBulkData1[0]); + done(); + }); + }); + + it('generateUrlsBulk(): should generate upload and download urls for multiple files', (done) => { + spenderPlatformV1ApiService.post.and.returnValue(of({ data: generateUrlsBulkData1 })); + + service.generateUrlsBulk(['fi']).subscribe((response) => { + expect(response).toEqual(generateUrlsBulkData1); + done(); + }); + }); + + it('downloadFile(): should download file', (done) => { + spenderPlatformV1ApiService.get.and.returnValue(of({})); + + service.downloadFile('fi').subscribe((response) => { + expect(response).toEqual({}); + done(); + }); + }); }); diff --git a/src/app/core/services/platform/v1/spender/file.service.ts b/src/app/core/services/platform/v1/spender/file.service.ts index 9dcad5634e..9475d19990 100644 --- a/src/app/core/services/platform/v1/spender/file.service.ts +++ b/src/app/core/services/platform/v1/spender/file.service.ts @@ -42,7 +42,7 @@ export class SpenderFileService { .pipe(map((response) => response.data)); } - downloadFile(id: string): {} { + downloadFile(id: string): Observable<{}> { return this.spenderPlatformV1ApiService.get('/files/download?id=' + id); } } diff --git a/src/app/fyle/view-expense/view-expense.page.spec.ts b/src/app/fyle/view-expense/view-expense.page.spec.ts index 8caa2878db..288f6e30a5 100644 --- a/src/app/fyle/view-expense/view-expense.page.spec.ts +++ b/src/app/fyle/view-expense/view-expense.page.spec.ts @@ -49,6 +49,9 @@ import { ExpenseState } from 'src/app/core/models/expense-state.enum'; import { TransactionStatusInfoPopoverComponent } from 'src/app/shared/components/transaction-status-info-popover/transaction-status-info-popover.component'; import { OrgSettings } from 'src/app/core/models/org-settings.model'; import { CustomInput } from 'src/app/core/models/custom-input.model'; +import { SpenderFileService } from 'src/app/core/services/platform/v1/spender/file.service'; +import { ApproverFileService } from 'src/app/core/services/platform/v1/approver/file.service'; +import { urlsBulkData } from 'src/app/core/mock-data/platform-file.data'; import { ApproverReportsService } from 'src/app/core/services/platform/v1/approver/reports.service'; import { expectedReportsSinglePage, @@ -77,6 +80,8 @@ describe('ViewExpensePage', () => { let dependentFieldsService: jasmine.SpyObj; let approverExpensesService: jasmine.SpyObj; let spenderExpensesService: jasmine.SpyObj; + let spenderFileService: jasmine.SpyObj; + let approverFileService: jasmine.SpyObj; let activateRouteMock: ActivatedRoute; let approverReportsService: jasmine.SpyObj; @@ -127,6 +132,9 @@ describe('ViewExpensePage', () => { 'getReportById', ]); + const spenderFileServiceSpy = jasmine.createSpyObj('SpenderFileService', ['generateUrlsBulk']); + const approverFileServiceSpy = jasmine.createSpyObj('ApproverFileService', ['generateUrlsBulk']); + TestBed.configureTestingModule({ declarations: [ViewExpensePage], imports: [IonicModule.forRoot(), FormsModule, MatIconModule, MatIconTestingModule], @@ -203,6 +211,14 @@ describe('ViewExpensePage', () => { useValue: spenderExpensesServiceSpy, provide: SpenderExpensesService, }, + { + useValue: spenderFileServiceSpy, + provide: SpenderFileService, + }, + { + useValue: approverFileServiceSpy, + provide: ApproverFileService, + }, { provide: ApproverReportsService, useValue: approverReportsServiceSpy, @@ -244,6 +260,8 @@ describe('ViewExpensePage', () => { loaderService = TestBed.inject(LoaderService) as jasmine.SpyObj; approverExpensesService = TestBed.inject(ApproverExpensesService) as jasmine.SpyObj; spenderExpensesService = TestBed.inject(SpenderExpensesService) as jasmine.SpyObj; + spenderFileService = TestBed.inject(SpenderFileService) as jasmine.SpyObj; + approverFileService = TestBed.inject(ApproverFileService) as jasmine.SpyObj; approverReportsService = TestBed.inject(ApproverReportsService) as jasmine.SpyObj; activateRouteMock = TestBed.inject(ActivatedRoute); @@ -882,26 +900,30 @@ describe('ViewExpensePage', () => { it('should be able to edit expense attachments', fakeAsync(() => { spyOn(component.updateFlag$, 'next'); + component.view = ExpenseView.team; - fileService.getReceiptsDetails.and.returnValue({ + const details = { + url: 'mock-url', type: 'image', thumbnail: 'mock-thumbnail', - }); - - const mockDownloadUrl = { - url: 'mock-url', }; - fileService.downloadUrl.and.returnValue(of(mockDownloadUrl.url)); + + fileService.getReceiptsDetails.and.returnValue(details); + approverFileService.generateUrlsBulk.and.returnValue(of(urlsBulkData)); + component.ionViewWillEnter(); tick(500); component.expense$.subscribe((expense) => { - expect(fileService.downloadUrl).toHaveBeenCalledOnceWith(fileObjectData.id); - expect(fileService.getReceiptsDetails).toHaveBeenCalledOnceWith(fileObjectData.name, fileObjectData.url); + expect(approverFileService.generateUrlsBulk).toHaveBeenCalledOnceWith(expense.file_ids); + expect(fileService.getReceiptsDetails).toHaveBeenCalledOnceWith( + urlsBulkData[0].name, + urlsBulkData[0].download_url + ); }); tick(500); expect(component.updateFlag$.next).toHaveBeenCalledOnceWith(null); component.attachments$.subscribe((attachments) => { - expect(attachments).toEqual([fileObjectData]); + expect(attachments).toEqual([details]); expect(component.isLoading).toBeFalse(); }); })); diff --git a/src/app/fyle/view-expense/view-expense.page.ts b/src/app/fyle/view-expense/view-expense.page.ts index 44873773ba..b824acdf09 100644 --- a/src/app/fyle/view-expense/view-expense.page.ts +++ b/src/app/fyle/view-expense/view-expense.page.ts @@ -1,10 +1,10 @@ import { Component, EventEmitter, ViewChild, ElementRef } from '@angular/core'; -import { Observable, from, Subject, concat, noop, forkJoin } from 'rxjs'; +import { Observable, from, Subject, concat, noop, forkJoin, of } from 'rxjs'; import { LoaderService } from 'src/app/core/services/loader.service'; import { TransactionService } from 'src/app/core/services/transaction.service'; import { ActivatedRoute, Router } from '@angular/router'; import { CustomInputsService } from 'src/app/core/services/custom-inputs.service'; -import { switchMap, shareReplay, concatMap, map, finalize, reduce, takeUntil, take, filter } from 'rxjs/operators'; +import { switchMap, shareReplay, concatMap, map, finalize, takeUntil, take, filter } from 'rxjs/operators'; import { StatusService } from 'src/app/core/services/status.service'; import { FileService } from 'src/app/core/services/file.service'; import { ModalController, PopoverController } from '@ionic/angular'; @@ -34,6 +34,9 @@ import { Expense, TransactionStatus } from 'src/app/core/models/platform/v1/expe import { AccountType } from 'src/app/core/models/platform/v1/account.model'; import { ExpenseState } from 'src/app/core/models/expense-state.enum'; import { TransactionStatusInfoPopoverComponent } from 'src/app/shared/components/transaction-status-info-popover/transaction-status-info-popover.component'; +import { SpenderFileService } from 'src/app/core/services/platform/v1/spender/file.service'; +import { ApproverFileService } from 'src/app/core/services/platform/v1/approver/file.service'; +import { PlatformFileGenerateUrlsResponse } from 'src/app/core/models/platform/platform-file-generate-urls-response.model'; import { ApproverReportsService } from 'src/app/core/services/platform/v1/approver/reports.service'; @Component({ @@ -150,6 +153,8 @@ export class ViewExpensePage { private dependentFieldsService: DependentFieldsService, private spenderExpensesService: SpenderExpensesService, private approverExpensesService: ApproverExpensesService, + private spenderFileService: SpenderFileService, + private approverFileService: ApproverFileService, private approverReportsService: ApproverReportsService ) {} @@ -411,22 +416,33 @@ export class ViewExpensePage { const editExpenseAttachments = this.expense$.pipe( take(1), - switchMap((expense) => from(expense.files)), - concatMap((fileObj) => - this.fileService.downloadUrl(fileObj.id).pipe( - map((downloadUrl) => { - const details = this.fileService.getReceiptsDetails(fileObj.name, downloadUrl); - const fileObjWithDetails: FileObject = { - url: downloadUrl, - type: details.type, - thumbnail: details.thumbnail, - }; + switchMap((expense) => { + if (expense.file_ids.length > 0) { + if (this.view === ExpenseView.individual) { + return this.spenderFileService.generateUrlsBulk(expense.file_ids); + } else { + return this.approverFileService.generateUrlsBulk(expense.file_ids); + } + } else { + return of([]); + } + }), + map((response: PlatformFileGenerateUrlsResponse[]) => { + const files = response.filter((file) => file.content_type !== 'text/html'); + const fileObjs = files.map((obj) => { + const details = this.fileService.getReceiptsDetails(obj.name, obj.download_url); + + const fileObj: FileObject = { + url: obj.download_url, + type: details.type, + thumbnail: details.thumbnail, + }; + + return fileObj; + }); - return fileObjWithDetails; - }) - ) - ), - reduce((acc: FileObject[], curr) => acc.concat(curr), []) + return fileObjs; + }) ); this.attachments$ = editExpenseAttachments; diff --git a/src/app/fyle/view-mileage/view-mileage.page.ts b/src/app/fyle/view-mileage/view-mileage.page.ts index e6370b4977..66caa41b6c 100644 --- a/src/app/fyle/view-mileage/view-mileage.page.ts +++ b/src/app/fyle/view-mileage/view-mileage.page.ts @@ -400,8 +400,8 @@ export class ViewMileagePage { forkJoin([this.expenseFields$, this.mileageExpense$.pipe(take(1))]) .pipe( map(([expenseFieldsMap, expense]) => { - this.projectFieldName = expenseFieldsMap?.project_id && expenseFieldsMap?.project_id[0]?.field_name; - const isProjectMandatory = expenseFieldsMap?.project_id && expenseFieldsMap?.project_id[0]?.is_mandatory; + this.projectFieldName = expenseFieldsMap?.project_id && expenseFieldsMap.project_id[0]?.field_name; + const isProjectMandatory = expenseFieldsMap?.project_id && expenseFieldsMap.project_id[0]?.is_mandatory; this.isProjectShown = this.orgSettings?.projects?.enabled && (!!expense.project?.name || isProjectMandatory); }) )