diff --git a/src/app/core/services/platform/v1/spender/expenses.service.ts b/src/app/core/services/platform/v1/spender/expenses.service.ts index c55e55d60c..af67b24cf6 100644 --- a/src/app/core/services/platform/v1/spender/expenses.service.ts +++ b/src/app/core/services/platform/v1/spender/expenses.service.ts @@ -21,6 +21,7 @@ import { corporateCardTransaction } from 'src/app/core/models/platform/v1/cc-tra import { MatchedCorporateCardTransaction } from 'src/app/core/models/platform/v1/matched-corpporate-card-transaction.model'; import { MileageUnitEnum } from 'src/app/core/models/platform/platform-mileage-rates.model'; import { Location } from 'src/app/core/models/location.model'; +import { CommuteDeduction } from 'src/app/core/enums/commute-deduction.enum'; @Injectable({ providedIn: 'root', @@ -316,7 +317,11 @@ export class ExpensesService { custom_fields: transaction.custom_properties, per_diem_rate_id: transaction.per_diem_rate_id, per_diem_num_days: transaction.num_days || 0, - mileage_rate_id: transaction.mileage_rate_id, + mileage_rate_id: transaction.mileage_rate_id, // @arjun check if this is present + commute_deduction: transaction.commute_deduction as CommuteDeduction, + mileage_is_round_trip: transaction.mileage_is_round_trip, + commute_details_id: transaction.commute_details_id, + hotel_is_breakfast_provided: transaction.hotel_is_breakfast_provided, advance_wallet_id: transaction.advance_wallet_id, file_ids: fileIds, report_id: transaction.report_id, diff --git a/src/app/core/services/transaction.service.spec.ts b/src/app/core/services/transaction.service.spec.ts index 08901d301d..1844f0e6d7 100644 --- a/src/app/core/services/transaction.service.spec.ts +++ b/src/app/core/services/transaction.service.spec.ts @@ -1115,7 +1115,7 @@ describe('TransactionService', () => { spyOn(transactionService, 'transformExpense').and.returnValue({ tx: txnData2 }); transactionService.createTxnWithFiles(txnWithSourceOnly, of(mockFileObject)).subscribe((res) => { - expect(expensesService.createFromFile).toHaveBeenCalledOnceWith(mockFileObject[0].id, 'TPA'); + expect(expensesService.createFromFile).toHaveBeenCalledOnceWith(mockFileObject[0].id, 'MOBILE_DASHCAM'); expect(transactionService.transformExpense).toHaveBeenCalled(); expect(res).toEqual(txnData2); done(); diff --git a/src/app/core/services/transaction.service.ts b/src/app/core/services/transaction.service.ts index 27c9e63707..1c4f7b71c2 100644 --- a/src/app/core/services/transaction.service.ts +++ b/src/app/core/services/transaction.service.ts @@ -199,12 +199,9 @@ export class TransactionService { if (txn.hasOwnProperty('source') && Object.keys(txn).length === 1) { const fileIds = fileObjs.map((fileObj) => fileObj.id); if (fileIds.length > 0) { - return ( - this.expensesService - // todo @arjun to change the source to txn.source later when the backend changes are live - .createFromFile(fileIds[0], 'TPA') - .pipe(map((result) => this.transformExpense(result.data[0]).tx)) - ); + return this.expensesService + .createFromFile(fileIds[0], txn.source) + .pipe(map((result) => this.transformExpense(result.data[0]).tx)); } } else { const fileIds = fileObjs.map((fileObj) => fileObj.id); diff --git a/src/app/core/services/transactions-outbox.service.spec.ts b/src/app/core/services/transactions-outbox.service.spec.ts index 9cbd7d5e7c..0b6baa15a6 100644 --- a/src/app/core/services/transactions-outbox.service.spec.ts +++ b/src/app/core/services/transactions-outbox.service.spec.ts @@ -1,8 +1,7 @@ import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { TestBed, fakeAsync, tick } from '@angular/core/testing'; import { expenseList2 } from '../mock-data/expense.data'; -import { fileObject4, fileObjectData1 } from '../mock-data/file-object.data'; -import { editUnflattenedTransaction, txnData2 } from '../mock-data/transaction.data'; +import { txnData2 } from '../mock-data/transaction.data'; import { DateService } from './date.service'; import { FileService } from './file.service'; import { OrgUserSettingsService } from './org-user-settings.service'; @@ -11,13 +10,10 @@ import { StorageService } from './storage.service'; import { TrackingService } from './tracking.service'; import { TransactionService } from './transaction.service'; import { ExpensesService } from './platform/v1/spender/expenses.service'; - import { TransactionsOutboxService } from './transactions-outbox.service'; import { outboxQueueData1 } from '../mock-data/outbox-queue.data'; import { cloneDeep } from 'lodash'; import { of } from 'rxjs'; -import { extractedData, parsedReceiptData1, parsedReceiptData2 } from '../mock-data/parsed-receipt.data'; -import { fileData1 } from '../mock-data/file.data'; import { SpenderReportsService } from './platform/v1/spender/reports.service'; import { parsedResponseData1 } from '../mock-data/parsed-response.data'; @@ -107,17 +103,6 @@ describe('TransactionsOutboxService', () => { expect(storageService.set).toHaveBeenCalledOnceWith('outbox', transactionsOutboxService.queue); }); - it('saveDataExtractionQueue(): should save data extraction queue', () => { - const mockQueue = cloneDeep(outboxQueueData1); - transactionsOutboxService.dataExtractionQueue = mockQueue; - storageService.set.and.resolveTo(null); - transactionsOutboxService.saveDataExtractionQueue(); - expect(storageService.set).toHaveBeenCalledOnceWith( - 'data_extraction_queue', - transactionsOutboxService.dataExtractionQueue - ); - }); - describe('isSyncInProgress', () => { it('should return true if sync is in progress', () => { transactionsOutboxService.syncInProgress = true; @@ -146,42 +131,6 @@ describe('TransactionsOutboxService', () => { }); }); - describe('isDataExtractionPending():', () => { - const txnId = 'tx3qHxFNgRcZ'; - it('should return true if data extraction is pending', () => { - const mockQueue = cloneDeep(outboxQueueData1); - transactionsOutboxService.dataExtractionQueue = mockQueue; - const res = transactionsOutboxService.isDataExtractionPending(txnId); - expect(res).toBeTrue(); - }); - - it('should return false if data extraction is not pending', () => { - transactionsOutboxService.dataExtractionQueue = []; - const res = transactionsOutboxService.isDataExtractionPending(txnId); - expect(res).toBeFalse(); - }); - }); - - it('removeDataExtractionEntry():', async () => { - const mockQueue = cloneDeep(outboxQueueData1); - transactionsOutboxService.dataExtractionQueue = mockQueue; - spyOn(transactionsOutboxService, 'saveDataExtractionQueue').and.resolveTo(); - await transactionsOutboxService.removeDataExtractionEntry(txnData2, [ - { url: '2023-02-08/orNVthTo2Zyo/receipts/fi6PQ6z4w6ET.000.jpeg', type: 'image/jpeg' }, - ]); - expect(transactionsOutboxService.saveDataExtractionQueue).toHaveBeenCalledTimes(1); - expect(transactionsOutboxService.dataExtractionQueue.length).toEqual(0); - }); - - it('addDataExtractionEntry():', () => { - spyOn(transactionsOutboxService, 'saveDataExtractionQueue').and.resolveTo(); - transactionsOutboxService.addDataExtractionEntry(txnData2, [ - { url: '2023-02-08/orNVthTo2Zyo/receipts/fi6PQ6z4w6ET.000.jpeg', type: 'image/jpeg' }, - ]); - expect(transactionsOutboxService.dataExtractionQueue.length).toEqual(1); - expect(transactionsOutboxService.saveDataExtractionQueue).toHaveBeenCalledTimes(1); - }); - it('uploadData(): should upload data', (done) => { const fileId = 'fiHPZUiichAS'; const uploadUrl = `${rootUrl}/files/${fileId}/upload_url`; @@ -230,15 +179,13 @@ describe('TransactionsOutboxService', () => { txnData2, [{ url: '2023-02-08/orNVthTo2Zyo/receipts/fi6PQ6z4w6ET.000.jpeg', type: 'image/jpeg' }], null, - null, - false + null ); expect(transactionsOutboxService.addEntry).toHaveBeenCalledOnceWith( txnData2, [{ url: '2023-02-08/orNVthTo2Zyo/receipts/fi6PQ6z4w6ET.000.jpeg', type: 'image/jpeg' }], null, - null, - false + null ); expect(transactionsOutboxService.syncEntry).toHaveBeenCalledOnceWith(outboxQueueData1[0]); expect(transactionsOutboxService.queue.length).toEqual(0); @@ -255,8 +202,7 @@ describe('TransactionsOutboxService', () => { txnData2, [{ url: '2023-02-08/orNVthTo2Zyo/receipts/fi6PQ6z4w6ET.000.jpeg', type: 'image/jpeg' }], null, - null, - false + null ); expect(transactionsOutboxService.syncEntry).toHaveBeenCalledOnceWith(outboxQueueData1[0]); expect(transactionsOutboxService.queue.length).toEqual(0); @@ -317,9 +263,8 @@ describe('TransactionsOutboxService', () => { tick(100); expect(storageService.get).toHaveBeenCalledWith('outbox'); - expect(storageService.get).toHaveBeenCalledWith('data_extraction_queue'); expect(transactionsOutboxService.queue).toEqual(mockQueue); - expect(dateService.fixDates).toHaveBeenCalledTimes(2); + expect(dateService.fixDates).toHaveBeenCalledTimes(1); expect(dateService.fixDates).toHaveBeenCalledWith(mockQueue[0].transaction); })); @@ -329,32 +274,11 @@ describe('TransactionsOutboxService', () => { tick(100); expect(storageService.get).toHaveBeenCalledWith('outbox'); - expect(storageService.get).toHaveBeenCalledWith('data_extraction_queue'); expect(transactionsOutboxService.queue).toEqual([]); expect(dateService.fixDates).not.toHaveBeenCalled(); })); }); - it('processDataExtractionEntry(): should process data extraction entry', fakeAsync(() => { - const mockQueue = cloneDeep([...outboxQueueData1, ...outboxQueueData1, ...outboxQueueData1]); - transactionsOutboxService.dataExtractionQueue = cloneDeep(mockQueue); - transactionService.upsert.and.returnValue(of(editUnflattenedTransaction)); - spyOn(transactionsOutboxService, 'parseReceipt').and.returnValues( - Promise.resolve(parsedReceiptData1), - Promise.resolve({ data: undefined }), - Promise.resolve(parsedReceiptData2) - ); - spyOn(transactionsOutboxService, 'removeDataExtractionEntry').and.resolveTo(); - spyOn(transactionsOutboxService, 'addEntryAndSync').and.resolveTo(outboxQueueData1[0]); - - transactionsOutboxService.processDataExtractionEntry(); - tick(100); - - expect(transactionsOutboxService.parseReceipt).toHaveBeenCalledTimes(3); - expect(transactionService.upsert).toHaveBeenCalledTimes(2); - expect(transactionsOutboxService.removeDataExtractionEntry).toHaveBeenCalledTimes(3); - })); - describe('getExpenseDate():', () => { it('should return transaction date if txn_dt is present', () => { const txnDate = new Date('2023-02-15T06:30:00.000Z'); diff --git a/src/app/core/services/transactions-outbox.service.ts b/src/app/core/services/transactions-outbox.service.ts index fa9469b178..8ad12f30ca 100644 --- a/src/app/core/services/transactions-outbox.service.ts +++ b/src/app/core/services/transactions-outbox.service.ts @@ -5,19 +5,15 @@ import { Observable, from, noop } from 'rxjs'; import { environment } from 'src/environments/environment'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { TransactionService } from './transaction.service'; -import { FileService } from './file.service'; import { StatusService } from './status.service'; import { indexOf } from 'lodash'; import { ParsedReceipt } from '../models/parsed_receipt.model'; import { TrackingService } from './tracking.service'; import { Expense } from '../models/expense.model'; import { CurrencyService } from './currency.service'; -import { OrgUserSettingsService } from './org-user-settings.service'; import { Transaction } from '../models/v1/transaction.model'; import { FileObject } from '../models/file-obj.model'; import { OutboxQueue } from '../models/outbox-queue.model'; -import { ExpensesService } from './platform/v1/spender/expenses.service'; -import { SpenderReportsService } from './platform/v1/spender/reports.service'; import { ParsedResponse } from '../models/parsed_response.model'; import { SpenderFileService } from './platform/v1/spender/file.service'; import { PlatformFile } from '../models/platform/platform-file.model'; @@ -207,7 +203,6 @@ export class TransactionsOutboxService { syncEntry(entry: OutboxQueue): Promise { const that = this; const fileObjPromiseArray: Promise[] = []; - const reportId = entry.reportId; if (!entry.fileUploadCompleted) { if (entry.dataUrls && entry.dataUrls.length > 0) { diff --git a/src/app/fyle/add-edit-mileage/add-edit-mileage.page.ts b/src/app/fyle/add-edit-mileage/add-edit-mileage.page.ts index e3d796d55d..55f6a7dcfb 100644 --- a/src/app/fyle/add-edit-mileage/add-edit-mileage.page.ts +++ b/src/app/fyle/add-edit-mileage/add-edit-mileage.page.ts @@ -2335,7 +2335,7 @@ export class AddEditMileagePage implements OnInit { switchMap((rates) => { const transactionCopy = cloneDeep(etxn.tx) as PublicPolicyExpense; const selectedMileageRate = this.getMileageByVehicleType(rates, etxn.tx.mileage_vehicle_type); - transactionCopy.mileage_rate_id = selectedMileageRate.id; + transactionCopy.mileage_rate_id = selectedMileageRate?.id; /* Expense creation has not moved to platform yet and since policy is moved to platform, * it expects the expense object in terms of platform world. Until then, the method 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 4f7ce7f288..873e89ee66 100644 --- a/src/app/fyle/my-expenses/my-expenses.page.spec.ts +++ b/src/app/fyle/my-expenses/my-expenses.page.spec.ts @@ -1,6 +1,7 @@ -import { ComponentFixture, TestBed, fakeAsync, tick, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed, discardPeriodicTasks, fakeAsync, tick, waitForAsync } from '@angular/core/testing'; import { ActionSheetController, IonicModule, ModalController, NavController, PopoverController } from '@ionic/angular'; +import * as dayjs from 'dayjs'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { MatBottomSheet, MatBottomSheetRef } from '@angular/material/bottom-sheet'; @@ -18,22 +19,14 @@ import { } from 'src/app/core/mock-data/action-sheet-options.data'; import { allowedExpenseTypes } from 'src/app/core/mock-data/allowed-expense-types.data'; import { apiAuthRes } from 'src/app/core/mock-data/auth-reponse.data'; -import { cardAggregateStatParam } from 'src/app/core/mock-data/card-aggregate-stats.data'; import { expectedAssignedCCCStats } from 'src/app/core/mock-data/ccc-expense.details.data'; import { - expectedCriticalPolicyViolationPopoverParams, - expectedCriticalPolicyViolationPopoverParams2, - expectedCriticalPolicyViolationPopoverParams3, -} from 'src/app/core/mock-data/critical-policy-violation-popover.data'; -import { - expenseFiltersData1, expenseFiltersData2, expenseWithPotentialDuplicateFilterData, } from 'src/app/core/mock-data/expense-filters.data'; import { apiExpenseRes, expectedFormattedTransaction, - expenseData1, expenseData2, expenseList4, expenseListwithoutID, @@ -82,8 +75,6 @@ import { mileageExpenseWithDistance, perDiemExpenseWithSingleNumDays, } from 'src/app/core/mock-data/platform/v1/expense.data'; -import { reportUnflattenedData } from 'src/app/core/mock-data/report-v1.data'; -import { expectedReportSingleResponse } from 'src/app/core/mock-data/report.data'; import { selectedFilters1, selectedFilters2 } from 'src/app/core/mock-data/selected-filters.data'; import { snackbarPropertiesRes, @@ -92,15 +83,11 @@ import { snackbarPropertiesRes4, } from 'src/app/core/mock-data/snackbar-properties.data'; import { txnList } from 'src/app/core/mock-data/transaction.data'; -import { unflattenedTxnData } from 'src/app/core/mock-data/unflattened-txn.data'; import { unformattedTxnData } from 'src/app/core/mock-data/unformatted-transaction.data'; -import { expectedUniqueCardStats } from 'src/app/core/mock-data/unique-cards-stats.data'; import { uniqueCardsData } from 'src/app/core/mock-data/unique-cards.data'; import { AdvancesStates } from 'src/app/core/models/advances-states.model'; import { BackButtonActionPriority } from 'src/app/core/models/back-button-action-priority.enum'; -import { Expense } from 'src/app/core/models/expense.model'; import { ExtendedReport } from 'src/app/core/models/report.model'; -import { ApiV2Service } from 'src/app/core/services/api-v2.service'; import { CategoriesService } from 'src/app/core/services/categories.service'; import { CorporateCreditCardExpenseService } from 'src/app/core/services/corporate-credit-card-expense.service'; import { CurrencyService } from 'src/app/core/services/currency.service'; @@ -143,13 +130,14 @@ import { UtilityService } from 'src/app/core/services/utility.service'; import { AuthService } from 'src/app/core/services/auth.service'; import { apiEouRes } from 'src/app/core/mock-data/extended-org-user.data'; import { properties } from 'src/app/core/mock-data/modal-properties.data'; +import { ExpensesQueryParams } from 'src/app/core/models/platform/v1/expenses-query-params.model'; +import { Expense } from 'src/app/core/models/platform/v1/expense.model'; describe('MyExpensesPage', () => { let component: MyExpensesPage; let fixture: ComponentFixture; let tasksService: jasmine.SpyObj; let currencyService: jasmine.SpyObj; - let apiV2Service: jasmine.SpyObj; let transactionService: jasmine.SpyObj; let orgSettingsService: jasmine.SpyObj; let activatedRoute: jasmine.SpyObj; @@ -185,7 +173,6 @@ describe('MyExpensesPage', () => { beforeEach(waitForAsync(() => { const tasksServiceSpy = jasmine.createSpyObj('TasksService', ['getReportsTaskCount', 'getExpensesTaskCount']); const currencyServiceSpy = jasmine.createSpyObj('CurrencyService', ['getHomeCurrency']); - const apiV2ServiceSpy = jasmine.createSpyObj('ApiV2Service', ['extendQueryParamsForTextSearch']); const transactionServiceSpy = jasmine.createSpyObj('TransactionService', [ 'getMyExpensesCount', 'getMyExpenses', @@ -322,7 +309,6 @@ describe('MyExpensesPage', () => { providers: [ { provide: TasksService, useValue: tasksServiceSpy }, { provide: CurrencyService, useValue: currencyServiceSpy }, - { provide: ApiV2Service, useValue: apiV2ServiceSpy }, { provide: TransactionService, useValue: transactionServiceSpy }, { provide: OrgSettingsService, useValue: orgSettingsServiceSpy }, { provide: ActivatedRoute, useValue: activatedRouteSpy }, @@ -449,7 +435,6 @@ describe('MyExpensesPage', () => { 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; transactionOutboxService = TestBed.inject(TransactionsOutboxService) as jasmine.SpyObj; @@ -493,7 +478,7 @@ describe('MyExpensesPage', () => { describe('ionViewWillEnter(): ', () => { let backButtonSubscription: Subscription; - + const dEincompleteExpenseIds = ['txfCdl3TEZ7K', 'txfCdl3TEZ7l', 'txfCdl3TEZ7m']; beforeEach(() => { component.isConnected$ = of(true); backButtonSubscription = new Subscription(); @@ -508,6 +493,8 @@ describe('MyExpensesPage', () => { spyOn(component, 'setAllExpensesCountAndAmount'); spyOn(component, 'clearFilters'); spyOn(component, 'setupActionSheet'); + //@ts-ignore + spyOn(component, 'pollDEIncompleteExpenses').and.returnValue(of(apiExpenses1)); tokenService.getClusterDomain.and.resolveTo(apiAuthRes.cluster_domain); currencyService.getHomeCurrency.and.returnValue(of('USD')); expensesService.getExpenseStats.and.returnValue(of(completeStats)); @@ -988,12 +975,204 @@ describe('MyExpensesPage', () => { expect(expensesService.getExpenses).not.toHaveBeenCalled(); })); + + it('should call pollDEIncompleteExpenses if expenses not completed DE scan', () => { + //@ts-ignore + spyOn(component, 'filterDEIncompleteExpenses').and.returnValue(dEincompleteExpenseIds); + component.ionViewWillEnter(); + + //@ts-ignore + expect(component.pollDEIncompleteExpenses).toHaveBeenCalledWith(dEincompleteExpenseIds, apiExpenses1); + }); }); it('HeaderState(): should return the headerState', () => { expect(component.HeaderState).toEqual(HeaderState); }); + describe('pollDEIncompleteExpenses()', () => { + beforeEach(() => { + expensesService.getExpenses.and.returnValue(of(apiExpenses1)); + //@ts-ignore + spyOn(component, 'updateExpensesList').and.returnValue(apiExpenses1); + }); + + it('should call expenseService.getExpenses for dE incomplete expenses and return updated expenses', fakeAsync(() => { + const dEincompleteExpenseIds = ['txfCdl3TEZ7K', 'txfCdl3TEZ7l', 'txfCdl3TEZ7m']; + const dEincompleteExpenseIdParams: ExpensesQueryParams = { + queryParams: { id: `in.(${dEincompleteExpenseIds.join(',')})` }, + }; + + //@ts-ignore + component.pollDEIncompleteExpenses(dEincompleteExpenseIds, apiExpenses1).subscribe((result) => { + expect(expensesService.getExpenses).toHaveBeenCalledOnceWith({ ...dEincompleteExpenseIdParams.queryParams }); + expect(result).toEqual(apiExpenses1); + }); + tick(5000); + discardPeriodicTasks(); + })); + + it('should call expensesService.getExpenses 5 times and stop polling after 30 seconds', fakeAsync(() => { + const dEincompleteExpenseIds = ['txfCdl3TEZ7K', 'txfCdl3TEZ7l', 'txfCdl3TEZ7m']; + //@ts-ignore + spyOn(component, 'filterDEIncompleteExpenses').and.returnValue(dEincompleteExpenseIds); + //@ts-ignore + component.pollDEIncompleteExpenses(dEincompleteExpenseIds, apiExpenses1).subscribe(() => {}); + + // Simulate 30 seconds of time passing (the polling interval is 5 seconds) + tick(30000); + + // After 30 seconds, polling should stop, so no further calls to getAllExpenses + expect(expensesService.getExpenses).toHaveBeenCalledTimes(5); // If called every 5 seconds after the first 5 seconds + + // Cleanup + discardPeriodicTasks(); + })); + }); + + describe('updateExpensesList', () => { + beforeEach(() => { + //@ts-ignore + spyOn(component, 'isExpenseScanComplete').and.callThrough(); + }); + + it('should update expenses with completed scans', () => { + const updatedExpenses: Expense[] = [ + { ...apiExpenses1[0], extracted_data: { ...apiExpenses1[0].extracted_data, amount: 200 } }, + ]; + const dEincompleteExpenseIds = [apiExpenses1[0].id]; + + //@ts-ignore + component.isExpenseScanComplete.and.returnValue(true); + + //@ts-ignore + const result = component.updateExpensesList(apiExpenses1, updatedExpenses, dEincompleteExpenseIds); + + expect(result).toEqual([updatedExpenses[0], apiExpenses1[1]]); + }); + + it('should not update expenses if scan is incomplete', () => { + const updatedExpenses: Expense[] = [ + { ...apiExpenses1[0], extracted_data: { ...apiExpenses1[0].extracted_data, amount: 200 } }, + ]; + const dEincompleteExpenseIds = [apiExpenses1[0].id]; + + // Mock isExpenseScanComplete to return false for the updated expense + //@ts-ignore + (component.isExpenseScanComplete as jasmine.Spy).and.returnValue(false); + + //@ts-ignore + const result = component.updateExpensesList(apiExpenses1, updatedExpenses, dEincompleteExpenseIds); + + // Assert + expect(result).toEqual(apiExpenses1); // No changes should occur + }); + }); + + describe('checkIfScanIsCompleted():', () => { + it('should check if scan is complete and return true if the expense amount is not null and no other data is present', () => { + const expense = { + ...expenseData, + amount: 100, + claim_amount: null, + extracted_data: null, + }; + //@ts-ignore + const result = component.isExpenseScanComplete(expense); + expect(result).toBeTrue(); + }); + + it('should check if scan is complete and return true if the expense user amount is present and no extracted data is available', () => { + const expense = { + ...expenseData, + amount: null, + claim_amount: 7500, + extracted_data: null, + }; + //@ts-ignore + const result = component.isExpenseScanComplete(expense); + expect(result).toBeTrue(); + }); + + it('should check if scan is complete and return true if the required extracted data is present', () => { + const expense = { + ...expenseData, + amount: null, + claim_amount: null, + extracted_data: { + amount: 84.12, + currency: 'USD', + category: 'Professional Services', + date: null, + vendor_name: null, + invoice_dt: null, + }, + }; + //@ts-ignore + const result = component.isExpenseScanComplete(expense); + expect(result).toBeTrue(); + }); + + it('should return true if the scan has expired', () => { + const expense = { + ...expenseData, + amount: null, + claim_amount: null, + extracted_data: null, + }; + const oneDaysAfter = dayjs(expense.created_at).add(1, 'day').toDate(); + jasmine.clock().mockDate(oneDaysAfter); + + //@ts-ignore + const result = component.isExpenseScanComplete(expense); + expect(result).toBeTrue(); + }); + }); + + describe('isZeroAmountPerDiemOrMileage():', () => { + it('should check if scan is complete and return true if it is per diem expense with amount 0', () => { + const expense = { + ...cloneDeep(expenseData), + amount: 0, + }; + expense.category.name = 'Per Diem'; + //@ts-ignore + const result = component.isZeroAmountPerDiemOrMileage(expense); + expect(result).toBeTrue(); + }); + + it('should check if scan is complete and return true if it is per diem expense with user amount 0', () => { + const expense = { + ...cloneDeep(expenseData), + amount: null, + claim_amount: 0, + }; + expense.category.name = 'Per Diem'; + //@ts-ignore + const result = component.isZeroAmountPerDiemOrMileage(expense); + expect(result).toBeTrue(); + }); + + it('should check if scan is complete and return true if it is mileage expense with amount 0', () => { + const expense = { + ...cloneDeep(expenseData), + amount: 0, + }; + expense.category.name = 'Mileage'; + //@ts-ignore + const result = component.isZeroAmountPerDiemOrMileage(expense); + expect(result).toBeTrue(); + }); + + it('should return false if org category is null', () => { + const expense = cloneDeep(expenseData); + expense.category.name = null; + //@ts-ignore + const result = component.isZeroAmountPerDiemOrMileage(expense); + expect(result).toBeFalse(); + }); + }); + describe('clearText', () => { let dispatchEventSpy: jasmine.Spy; beforeEach(() => { @@ -2947,7 +3126,7 @@ describe('MyExpensesPage', () => { expect(component.isReportableExpensesSelected).toBeTrue(); }); - it('should update selectedElements, allExpensesCount and call apiV2Service if checked is true', () => { + it('should update selectedElements, allExpensesCount and call expensesService if checked is true', () => { expensesService.getAllExpenses.and.returnValue(of(cloneDeep(apiExpenses1))); component.outboxExpensesToBeDeleted = apiExpenseRes; component.pendingTransactions = cloneDeep([]); diff --git a/src/app/fyle/my-expenses/my-expenses.page.ts b/src/app/fyle/my-expenses/my-expenses.page.ts index 2159878cfb..81cc3d51f2 100644 --- a/src/app/fyle/my-expenses/my-expenses.page.ts +++ b/src/app/fyle/my-expenses/my-expenses.page.ts @@ -4,7 +4,7 @@ import { MatBottomSheet } from '@angular/material/bottom-sheet'; import { MatSnackBar } from '@angular/material/snack-bar'; import { ActivatedRoute, NavigationStart, Params, Router } from '@angular/router'; import { ActionSheetController, ModalController, NavController, PopoverController } from '@ionic/angular'; -import { cloneDeep, isEqual } from 'lodash'; +import { cloneDeep, isEqual, isNumber } from 'lodash'; import { BehaviorSubject, Observable, @@ -17,17 +17,22 @@ import { iif, noop, of, + timer, } from 'rxjs'; import { debounceTime, distinctUntilChanged, + exhaustMap, filter, finalize, map, shareReplay, + startWith, switchMap, take, takeUntil, + takeWhile, + timeout, } from 'rxjs/operators'; import { BackButtonActionPriority } from 'src/app/core/models/back-button-action-priority.enum'; import { Expense } from 'src/app/core/models/expense.model'; @@ -38,7 +43,6 @@ import { Expense as PlatformExpense } from 'src/app/core/models/platform/v1/expe import { GetExpenseQueryParam } from 'src/app/core/models/platform/v1/get-expenses-query.model'; import { UniqueCards } from 'src/app/core/models/unique-cards.model'; import { Transaction } from 'src/app/core/models/v1/transaction.model'; -import { ApiV2Service } from 'src/app/core/services/api-v2.service'; import { CategoriesService } from 'src/app/core/services/categories.service'; import { CorporateCreditCardExpenseService } from 'src/app/core/services/corporate-credit-card-expense.service'; import { CurrencyService } from 'src/app/core/services/currency.service'; @@ -78,6 +82,8 @@ import { PromoteOptInModalComponent } from 'src/app/shared/components/promote-op import { AuthService } from 'src/app/core/services/auth.service'; import { UtilityService } from 'src/app/core/services/utility.service'; import { FeatureConfigService } from 'src/app/core/services/platform/v1/spender/feature-config.service'; +import * as dayjs from 'dayjs'; +import { ExpensesQueryParams } from 'src/app/core/models/platform/v1/expenses-query-params.model'; @Component({ selector: 'app-my-expenses', @@ -212,7 +218,6 @@ export class MyExpensesPage implements OnInit { private trackingService: TrackingService, private storageService: StorageService, private tokenService: TokenService, - private apiV2Service: ApiV2Service, private modalProperties: ModalPropertiesService, private matBottomSheet: MatBottomSheet, private matSnackBar: MatSnackBar, @@ -612,7 +617,24 @@ export class MyExpensesPage implements OnInit { }) ); - this.myExpenses$ = paginatedPipe.pipe(shareReplay(1)); + /** + * Observable that manages expenses, including polling for incomplete scans. + */ + this.myExpenses$ = paginatedPipe.pipe( + switchMap((expenses) => { + const dEincompleteExpenseIds = this.filterDEIncompleteExpenses(expenses); + + if (dEincompleteExpenseIds.length === 0) { + return of(expenses); // All scans are completed + } else { + return this.pollDEIncompleteExpenses(dEincompleteExpenseIds, expenses).pipe( + startWith(expenses), + timeout(30000) + ); + } + }), + shareReplay(1) + ); this.count$ = this.loadExpenses$.pipe( switchMap((params) => { @@ -1759,4 +1781,95 @@ export class MyExpensesPage implements OnInit { }) ); } + + private isZeroAmountPerDiemOrMileage(expense: PlatformExpense): boolean { + return ( + (expense.category.name?.toLowerCase() === 'per diem' || expense.category.name?.toLowerCase() === 'mileage') && + (expense.amount === 0 || expense.claim_amount === 0) + ); + } + + /** + * Checks if the scan process for an expense has been completed. + * @param PlatformExpense expense - The expense to check. + * @returns boolean - True if the scan is complete or if data is manually entered. + */ + private isExpenseScanComplete(expense: PlatformExpense): boolean { + const isZeroAmountPerDiemOrMileage = this.isZeroAmountPerDiemOrMileage(expense); + + const hasUserManuallyEnteredData = + isZeroAmountPerDiemOrMileage || + ((expense.amount || expense.claim_amount) && isNumber(expense.amount || expense.claim_amount)); + const isDataExtracted = !!expense.extracted_data; + + // this is to prevent the scan failed from being shown from an indefinite amount of time. + const hasScanExpired = expense.created_at && dayjs(expense.created_at).diff(Date.now(), 'day') < 0; + return !!(hasUserManuallyEnteredData || isDataExtracted || hasScanExpired); + } + + /** + * Filters the list of expenses to get only those with incomplete scans. + * @param PlatformExpense[] expenses - The list of expenses to check. + * @returns string[] - Array of expense IDs that have incomplete scans. + */ + private filterDEIncompleteExpenses(expenses: PlatformExpense[]): string[] { + return expenses.filter((expense) => !this.isExpenseScanComplete(expense)).map((expense) => expense.id); + } + + /** + * Updates the expenses with polling results. + * @param PlatformExpense[] initialExpenses - The initial list of expenses. + * @param PlatformExpense[] updatedExpenses - The updated expenses after polling. + * @param string[] dEincompleteExpenseIds - Array of expense IDs with incomplete scans. + * @returns PlatformExpense[] - Updated list of expenses. + */ + private updateExpensesList( + initialExpenses: PlatformExpense[], + updatedExpenses: PlatformExpense[], + dEincompleteExpenseIds: string[] + ): PlatformExpense[] { + const updatedExpensesMap = new Map(updatedExpenses.map((expense) => [expense.id, expense])); + + const newExpensesList = initialExpenses.map((expense) => { + if (dEincompleteExpenseIds.includes(expense.id)) { + const updatedExpense = updatedExpensesMap.get(expense.id); + if (this.isExpenseScanComplete(updatedExpense)) { + return updatedExpense; + } + } + return expense; + }); + + return newExpensesList; + } + + /** + * Polls for expenses that have incomplete scan data. + * @param dEincompleteExpenseIds - Array of expense IDs with incomplete scans. + * @param initialExpenses - The initial list of expenses. + * @returns - Observable that emits updated expenses. + */ + private pollDEIncompleteExpenses( + dEincompleteExpenseIds: string[], + expenses: PlatformExpense[] + ): Observable { + let updatedExpensesList = expenses; + // Create a stop signal that emits after 30 seconds + const stopPolling$ = timer(30000); + return timer(5000, 5000).pipe( + exhaustMap(() => { + const params: ExpensesQueryParams = { queryParams: { id: `in.(${dEincompleteExpenseIds.join(',')})` } }; + return this.expenseService.getExpenses({ ...params.queryParams }).pipe( + map((updatedExpenses) => { + updatedExpensesList = this.updateExpensesList(updatedExpensesList, updatedExpenses, dEincompleteExpenseIds); + dEincompleteExpenseIds = this.filterDEIncompleteExpenses(updatedExpenses); + return updatedExpensesList; + }) + ); + }), + takeWhile(() => dEincompleteExpenseIds.length > 0, true), + takeUntil(stopPolling$), + takeUntil(this.onPageExit$) + ); + } } diff --git a/src/app/shared/components/expenses-card-v2/expenses-card.component.spec.ts b/src/app/shared/components/expenses-card-v2/expenses-card.component.spec.ts index d08f6652de..519d95a720 100644 --- a/src/app/shared/components/expenses-card-v2/expenses-card.component.spec.ts +++ b/src/app/shared/components/expenses-card-v2/expenses-card.component.spec.ts @@ -35,7 +35,7 @@ import { CameraOptionsPopupComponent } from 'src/app/fyle/add-edit-expense/camer import { CaptureReceiptComponent } from 'src/app/shared/components/capture-receipt/capture-receipt.component'; import { ToastMessageComponent } from '../toast-message/toast-message.component'; import { DebugElement, EventEmitter } from '@angular/core'; -import { expenseData, expenseResponseData } from 'src/app/core/mock-data/platform/v1/expense.data'; +import { apiExpenses1, expenseData, expenseResponseData } from 'src/app/core/mock-data/platform/v1/expense.data'; import { AccountType } from 'src/app/core/models/platform/v1/account.model'; import { ExpensesService as SharedExpenseService } from 'src/app/core/services/platform/v1/shared/expenses.service'; import { PopupAlertComponent } from '../popup-alert/popup-alert.component'; @@ -88,7 +88,6 @@ describe('ExpensesCardComponent', () => { const popoverControllerSpy = jasmine.createSpyObj('PopoverController', ['create']); const networkServiceSpy = jasmine.createSpyObj('NetworkService', ['connectivityWatcher', 'isOnline']); const transactionsOutboxServiceSpy = jasmine.createSpyObj('TransactionsOutboxService', [ - 'isDataExtractionPending', 'isSyncInProgress', 'fileUpload', ]); @@ -155,7 +154,6 @@ describe('ExpensesCardComponent', () => { sharedExpenseService.isCriticalPolicyViolatedExpense.and.returnValue(false); platform.is.and.returnValue(true); fileService.getReceiptDetails.and.returnValue(fileObjectAdv[0].type); - transactionsOutboxService.isDataExtractionPending.and.returnValue(true); expensesService.getExpenseById.and.returnValue(of(platformExpenseData)); transactionService.transformExpense.and.returnValue(transformedExpenseData); networkService.isOnline.and.returnValue(of(true)); @@ -338,89 +336,36 @@ describe('ExpensesCardComponent', () => { }); }); - describe('pollDataExtractionStatus():', () => { - it('should call the callback when data extraction is not pending', fakeAsync(() => { - transactionsOutboxService.isDataExtractionPending.and.returnValue(false); - const callbackSpy = jasmine.createSpy('callback'); - component.pollDataExtractionStatus(callbackSpy); - tick(5000); - expect(callbackSpy).toHaveBeenCalledTimes(1); - })); - - it('should keep polling when data extraction is pending', fakeAsync(() => { - const callbackSpy = jasmine.createSpy('callback'); - - transactionsOutboxService.isDataExtractionPending.and.returnValue(true); - - component.pollDataExtractionStatus(callbackSpy); - tick(1000); // wait for the initial setTimeout call - - expect(transactionsOutboxService.isDataExtractionPending).toHaveBeenCalledTimes(1); - expect(callbackSpy).not.toHaveBeenCalledTimes(1); - - // simulate data extraction not pending - transactionsOutboxService.isDataExtractionPending.and.returnValue(false); - tick(5000); // wait for the next setTimeout call - - expect(transactionsOutboxService.isDataExtractionPending).toHaveBeenCalledTimes(2); - expect(callbackSpy).toHaveBeenCalledTimes(1); - })); - }); - describe('handleScanStatus():', () => { it('should handle status when the syncing is in progress and the extracted data is present', () => { component.isOutboxExpense = false; component.homeCurrency = 'INR'; - component.expense.id = 'txO6d6eiB4JF'; + component.expense = cloneDeep(apiExpenses1[0]); orgUserSettingsService.get.and.returnValue(of(orgUserSettingsData)); - const isScanCompletedSpy = spyOn(component, 'checkIfScanIsCompleted').and.returnValue(false); - expensesService.getExpenseById.and.returnValue(of(platformExpenseWithExtractedData)); - transactionService.transformExpense.and.returnValue(transformedExpenseWithExtractedData); - component.isScanInProgress = true; - spyOn(component, 'pollDataExtractionStatus').and.callFake((callback) => { - callback(); - }); - - transactionsOutboxService.isDataExtractionPending.and.returnValue(true); + const isScanCompletedSpy = spyOn(component, 'checkIfScanIsCompleted').and.returnValue(true); component.handleScanStatus(); expect(orgUserSettingsService.get).toHaveBeenCalledTimes(1); expect(isScanCompletedSpy).toHaveBeenCalledTimes(1); - expect(transactionsOutboxService.isDataExtractionPending).toHaveBeenCalledOnceWith('txO6d6eiB4JF'); - expect(component.pollDataExtractionStatus).toHaveBeenCalledTimes(1); - expect(expensesService.getExpenseById).toHaveBeenCalledOnceWith(component.expense.id); - expect(transactionService.transformExpense).toHaveBeenCalledOnceWith(platformExpenseWithExtractedData); expect(component.isScanCompleted).toBeTrue(); expect(component.isScanInProgress).toBeFalse(); - expect(component.expense.extracted_data).toEqual(transformedExpenseWithExtractedData.tx.extracted_data); }); it('should handle status when the sync is in progress and there is no extracted data present', () => { component.isOutboxExpense = false; - component.expense.id = 'txvslh8aQMbu'; + component.expense = cloneDeep(expenseData); orgUserSettingsService.get.and.returnValue(of(orgUserSettingsData)); const isScanCompletedSpy = spyOn(component, 'checkIfScanIsCompleted').and.returnValue(false); - expensesService.getExpenseById.and.returnValue(of(platformExpenseData)); - transactionService.transformExpense.and.returnValue(transformedExpenseData); component.isScanInProgress = true; - const pollDataSpy = spyOn(component, 'pollDataExtractionStatus').and.callFake((callback) => { - callback(); - }); - - transactionsOutboxService.isDataExtractionPending.and.returnValue(true); component.handleScanStatus(); expect(orgUserSettingsService.get).toHaveBeenCalledTimes(1); expect(component.checkIfScanIsCompleted).toHaveBeenCalledTimes(1); expect(isScanCompletedSpy).toHaveBeenCalledTimes(1); - expect(transactionsOutboxService.isDataExtractionPending).toHaveBeenCalledOnceWith('txvslh8aQMbu'); - expect(pollDataSpy).toHaveBeenCalledTimes(1); - expect(expensesService.getExpenseById).toHaveBeenCalledOnceWith(component.expense.id); - expect(transactionService.transformExpense).toHaveBeenCalledOnceWith(platformExpenseData); expect(component.isScanCompleted).toBeFalse(); - expect(component.isScanInProgress).toBeFalse(); + expect(component.isScanInProgress).toBeTrue(); }); it('should handle status when the scanning is not in progress', () => { diff --git a/src/app/shared/components/expenses-card-v2/expenses-card.component.ts b/src/app/shared/components/expenses-card-v2/expenses-card.component.ts index 8d2f22be59..29f671660e 100644 --- a/src/app/shared/components/expenses-card-v2/expenses-card.component.ts +++ b/src/app/shared/components/expenses-card-v2/expenses-card.component.ts @@ -216,7 +216,7 @@ export class ExpensesCardComponent implements OnInit { (that.homeCurrency === 'USD' || that.homeCurrency === 'INR') ) { that.isScanCompleted = that.checkIfScanIsCompleted(); - that.isScanInProgress = !that.isScanCompleted; + that.isScanInProgress = !that.isScanCompleted && !this.expense.extracted_data; } else { that.isScanCompleted = true; that.isScanInProgress = false; diff --git a/src/app/shared/components/expenses-card/expenses-card.component.spec.ts b/src/app/shared/components/expenses-card/expenses-card.component.spec.ts index e203098046..14c0bcb79a 100644 --- a/src/app/shared/components/expenses-card/expenses-card.component.spec.ts +++ b/src/app/shared/components/expenses-card/expenses-card.component.spec.ts @@ -21,7 +21,7 @@ import { FormsModule } from '@angular/forms'; import { ExpenseState } from '../../pipes/expense-state.pipe'; import { orgSettingsGetData } from 'src/app/core/test-data/org-settings.service.spec.data'; import { of, take } from 'rxjs'; -import { expenseData1 } from 'src/app/core/mock-data/expense.data'; +import { expenseData1, selectedExpense1 } from 'src/app/core/mock-data/expense.data'; import { apiExpenseRes } from 'src/app/core/mock-data/expense.data'; import { expenseFieldsMapResponse2 } from 'src/app/core/mock-data/expense-fields-map.data'; import { orgData1 } from 'src/app/core/mock-data/org.data'; @@ -85,7 +85,6 @@ describe('ExpensesCardComponent', () => { const popoverControllerSpy = jasmine.createSpyObj('PopoverController', ['create']); const networkServiceSpy = jasmine.createSpyObj('NetworkService', ['connectivityWatcher', 'isOnline']); const transactionsOutboxServiceSpy = jasmine.createSpyObj('TransactionsOutboxService', [ - 'isDataExtractionPending', 'isSyncInProgress', 'fileUpload', ]); @@ -150,7 +149,6 @@ describe('ExpensesCardComponent', () => { transactionService.getIsCriticalPolicyViolated.and.returnValue(false); platform.is.and.returnValue(true); fileService.getReceiptDetails.and.returnValue(fileObjectAdv[0].type); - transactionsOutboxService.isDataExtractionPending.and.returnValue(true); expensesService.getExpenseById.and.returnValue(of(platformExpenseData)); transactionService.transformExpense.and.returnValue(transformedExpenseData); networkService.isOnline.and.returnValue(of(true)); @@ -318,89 +316,37 @@ describe('ExpensesCardComponent', () => { }); }); - describe('pollDataExtractionStatus():', () => { - it('should call the callback when data extraction is not pending', fakeAsync(() => { - transactionsOutboxService.isDataExtractionPending.and.returnValue(false); - const callbackSpy = jasmine.createSpy('callback'); - component.pollDataExtractionStatus(callbackSpy); - tick(5000); - expect(callbackSpy).toHaveBeenCalledTimes(1); - })); - - it('should keep polling when data extraction is pending', fakeAsync(() => { - const callbackSpy = jasmine.createSpy('callback'); - - transactionsOutboxService.isDataExtractionPending.and.returnValue(true); - - component.pollDataExtractionStatus(callbackSpy); - tick(1000); // wait for the initial setTimeout call - - expect(transactionsOutboxService.isDataExtractionPending).toHaveBeenCalledTimes(1); - expect(callbackSpy).not.toHaveBeenCalledTimes(1); - - // simulate data extraction not pending - transactionsOutboxService.isDataExtractionPending.and.returnValue(false); - tick(5000); // wait for the next setTimeout call - - expect(transactionsOutboxService.isDataExtractionPending).toHaveBeenCalledTimes(2); - expect(callbackSpy).toHaveBeenCalledTimes(1); - })); - }); - describe('handleScanStatus():', () => { it('should handle status when the syncing is in progress and the extracted data is present', () => { component.isOutboxExpense = false; component.homeCurrency = 'INR'; - component.expense.tx_id = 'txO6d6eiB4JF'; + component.expense = selectedExpense1; orgUserSettingsService.get.and.returnValue(of(orgUserSettingsData)); const isScanCompletedSpy = spyOn(component, 'checkIfScanIsCompleted').and.returnValue(false); - expensesService.getExpenseById.and.returnValue(of(platformExpenseWithExtractedData)); - transactionService.transformExpense.and.returnValue(transformedExpenseWithExtractedData); component.isScanInProgress = true; - spyOn(component, 'pollDataExtractionStatus').and.callFake((callback) => { - callback(); - }); - - transactionsOutboxService.isDataExtractionPending.and.returnValue(true); component.handleScanStatus(); expect(orgUserSettingsService.get).toHaveBeenCalledTimes(1); expect(isScanCompletedSpy).toHaveBeenCalledTimes(1); - expect(transactionsOutboxService.isDataExtractionPending).toHaveBeenCalledOnceWith('txO6d6eiB4JF'); - expect(component.pollDataExtractionStatus).toHaveBeenCalledTimes(1); - expect(expensesService.getExpenseById).toHaveBeenCalledOnceWith(component.expense.tx_id); - expect(transactionService.transformExpense).toHaveBeenCalledOnceWith(platformExpenseWithExtractedData); - expect(component.isScanCompleted).toBeTrue(); + expect(component.isScanCompleted).toBeFalse(); expect(component.isScanInProgress).toBeFalse(); - expect(component.expense.tx_extracted_data).toEqual(transformedExpenseWithExtractedData.tx.extracted_data); }); it('should handle status when the sync is in progress and there is no extracted data present', () => { component.isOutboxExpense = false; - component.expense.tx_id = 'txvslh8aQMbu'; + component.expense = expenseData1; orgUserSettingsService.get.and.returnValue(of(orgUserSettingsData)); const isScanCompletedSpy = spyOn(component, 'checkIfScanIsCompleted').and.returnValue(false); - expensesService.getExpenseById.and.returnValue(of(platformExpenseData)); - transactionService.transformExpense.and.returnValue(transformedExpenseData); component.isScanInProgress = true; - const pollDataSpy = spyOn(component, 'pollDataExtractionStatus').and.callFake((callback) => { - callback(); - }); - - transactionsOutboxService.isDataExtractionPending.and.returnValue(true); component.handleScanStatus(); expect(orgUserSettingsService.get).toHaveBeenCalledTimes(1); expect(component.checkIfScanIsCompleted).toHaveBeenCalledTimes(1); expect(isScanCompletedSpy).toHaveBeenCalledTimes(1); - expect(transactionsOutboxService.isDataExtractionPending).toHaveBeenCalledOnceWith('txvslh8aQMbu'); - expect(pollDataSpy).toHaveBeenCalledTimes(1); - expect(expensesService.getExpenseById).toHaveBeenCalledOnceWith(component.expense.tx_id); - expect(transactionService.transformExpense).toHaveBeenCalledOnceWith(platformExpenseData); expect(component.isScanCompleted).toBeFalse(); - expect(component.isScanInProgress).toBeFalse(); + expect(component.isScanInProgress).toBeTrue(); }); it('should handle status when the scanning is not in progress', () => { 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 534450dbba..4f94d5c6bd 100644 --- a/src/app/shared/components/expenses-card/expenses-card.component.ts +++ b/src/app/shared/components/expenses-card/expenses-card.component.ts @@ -208,7 +208,7 @@ export class ExpensesCardComponent implements OnInit { (that.homeCurrency === 'USD' || that.homeCurrency === 'INR') ) { that.isScanCompleted = that.checkIfScanIsCompleted(); - that.isScanInProgress = !that.isScanCompleted; + that.isScanInProgress = !that.isScanCompleted && !this.expense.tx_extracted_data; } else { that.isScanCompleted = true; that.isScanInProgress = false;