From 2e009f4b49ee4a58bf33992192b9e57923457732 Mon Sep 17 00:00:00 2001 From: Suyash Patil <127177049+suyashpatil78@users.noreply.github.com> Date: Sat, 1 Jun 2024 11:52:15 +0530 Subject: [PATCH] fix: data extraction not working in case of bulk upload (#3010) * fix: data extraction not working in case of bulk upload * written test * test: fixed eslint issues (#3025) --- .../core/mock-data/parsed-response.data.ts | 11 +++ .../core/models/receipt-preview-data.model.ts | 7 ++ .../transactions-outbox.service.spec.ts | 29 +++++++- .../services/transactions-outbox.service.ts | 13 +++- .../capture-receipt.component.ts | 68 ++++++++++--------- .../components/sidemenu/sidemenu.component.ts | 4 -- 6 files changed, 93 insertions(+), 39 deletions(-) create mode 100644 src/app/core/mock-data/parsed-response.data.ts create mode 100644 src/app/core/models/receipt-preview-data.model.ts diff --git a/src/app/core/mock-data/parsed-response.data.ts b/src/app/core/mock-data/parsed-response.data.ts new file mode 100644 index 0000000000..c1f5ad1e8f --- /dev/null +++ b/src/app/core/mock-data/parsed-response.data.ts @@ -0,0 +1,11 @@ +import deepFreeze from 'deep-freeze-strict'; +import { ParsedResponse } from '../models/parsed_response.model'; + +export const parsedResponseData1: ParsedResponse = deepFreeze({ + category: 'SYSTEM', + currency: 'USD', + amount: 100, + date: new Date('2023-02-15T06:30:00.000Z'), + invoice_dt: new Date('2023-02-24T12:03:57.680Z'), + vendor_name: 'vendor', +}); diff --git a/src/app/core/models/receipt-preview-data.model.ts b/src/app/core/models/receipt-preview-data.model.ts new file mode 100644 index 0000000000..073b23f2c5 --- /dev/null +++ b/src/app/core/models/receipt-preview-data.model.ts @@ -0,0 +1,7 @@ +export interface ReceiptPreviewData { + base64ImagesWithSource: Partial<{ + source: string; + base64Image: string; + }>[]; + continueCaptureReceipt?: boolean; +} diff --git a/src/app/core/services/transactions-outbox.service.spec.ts b/src/app/core/services/transactions-outbox.service.spec.ts index 2126b30b62..9cbd7d5e7c 100644 --- a/src/app/core/services/transactions-outbox.service.spec.ts +++ b/src/app/core/services/transactions-outbox.service.spec.ts @@ -16,9 +16,10 @@ import { TransactionsOutboxService } from './transactions-outbox.service'; import { outboxQueueData1 } from '../mock-data/outbox-queue.data'; import { cloneDeep } from 'lodash'; import { of } from 'rxjs'; -import { parsedReceiptData1, parsedReceiptData2 } from '../mock-data/parsed-receipt.data'; +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'; describe('TransactionsOutboxService', () => { const rootUrl = 'https://staging.fyle.tech'; @@ -353,4 +354,30 @@ describe('TransactionsOutboxService', () => { 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'); + const mockQueue = cloneDeep(outboxQueueData1[0]); + mockQueue.transaction.txn_dt = txnDate; + const res = transactionsOutboxService.getExpenseDate(mockQueue, parsedResponseData1); + expect(res).toEqual(txnDate); + }); + + it('should return extracted date if txn_dt is not present', () => { + const mockQueue = cloneDeep(outboxQueueData1[0]); + mockQueue.transaction.txn_dt = null; + const res = transactionsOutboxService.getExpenseDate(mockQueue, parsedResponseData1); + expect(res).toEqual(parsedResponseData1.date); + }); + + it('should return today date if txn_dt and extracted date is not present', () => { + const mockQueue = cloneDeep(outboxQueueData1[0]); + mockQueue.transaction.txn_dt = null; + const mockParsedResponse = cloneDeep(parsedResponseData1); + mockParsedResponse.date = null; + const res = transactionsOutboxService.getExpenseDate(mockQueue, mockParsedResponse); + expect(res).toEqual(new Date()); + }); + }); }); diff --git a/src/app/core/services/transactions-outbox.service.ts b/src/app/core/services/transactions-outbox.service.ts index cba6b2ab10..01c3330477 100644 --- a/src/app/core/services/transactions-outbox.service.ts +++ b/src/app/core/services/transactions-outbox.service.ts @@ -19,6 +19,7 @@ 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'; @@ -131,6 +132,16 @@ export class TransactionsOutboxService { await this.saveDataExtractionQueue(); } + getExpenseDate(entry: OutboxQueue, extractedData: ParsedResponse): Date { + if (entry.transaction.txn_dt) { + return new Date(entry.transaction.txn_dt); + } else if (extractedData.date) { + return new Date(extractedData.date); + } else { + return new Date(); + } + } + async processDataExtractionEntry(): Promise { const that = this; const clonedQueue = cloneDeep(this.dataExtractionQueue); @@ -159,7 +170,7 @@ export class TransactionsOutboxService { }; entry.transaction.extracted_data = extractedData; - entry.transaction.txn_dt = new Date(); + entry.transaction.txn_dt = this.getExpenseDate(entry, parsedResponse); // TODO: add this to allow amout addtion to extracted expense // let transactionUpsertPromise; diff --git a/src/app/shared/components/capture-receipt/capture-receipt.component.ts b/src/app/shared/components/capture-receipt/capture-receipt.component.ts index ff137bae95..f896e8cd32 100644 --- a/src/app/shared/components/capture-receipt/capture-receipt.component.ts +++ b/src/app/shared/components/capture-receipt/capture-receipt.component.ts @@ -23,6 +23,7 @@ import { SnackbarPropertiesService } from 'src/app/core/services/snackbar-proper import { AuthService } from 'src/app/core/services/auth.service'; import { CameraService } from 'src/app/core/services/camera.service'; import { CameraPreviewService } from 'src/app/core/services/camera-preview.service'; +import { ReceiptPreviewData } from 'src/app/core/models/receipt-preview-data.model'; type Image = Partial<{ source: string; @@ -79,7 +80,7 @@ export class CaptureReceiptComponent implements OnInit, OnDestroy, AfterViewInit @Inject(DEVICE_PLATFORM) private devicePlatform: 'android' | 'ios' | 'web' ) {} - setupNetworkWatcher() { + setupNetworkWatcher(): void { const networkWatcherEmitter = new EventEmitter(); this.networkService.connectivityWatcher(networkWatcherEmitter); this.isOffline$ = concat(this.networkService.isOnline(), networkWatcherEmitter.asObservable()).pipe( @@ -88,21 +89,23 @@ export class CaptureReceiptComponent implements OnInit, OnDestroy, AfterViewInit ); } - ngOnInit() { + ngOnInit(): void { this.setupNetworkWatcher(); this.isBulkMode = false; this.base64ImagesWithSource = []; this.noOfReceipts = 0; } + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type addMultipleExpensesToQueue(base64ImagesWithSource: Image[]) { return from(base64ImagesWithSource).pipe( concatMap((res: Image) => this.addExpenseToQueue(res)), + // eslint-disable-next-line @typescript-eslint/no-unsafe-return reduce((acc, curr) => acc.concat(curr), []) ); } - addExpenseToQueue(base64ImagesWithSource: Image) { + addExpenseToQueue(base64ImagesWithSource: Image): Observable { let source = base64ImagesWithSource.source; return forkJoin({ @@ -115,7 +118,6 @@ export class CaptureReceiptComponent implements OnInit, OnDestroy, AfterViewInit } const transaction = { source, - txn_dt: new Date(), currency: eou?.org?.currency, }; @@ -131,7 +133,7 @@ export class CaptureReceiptComponent implements OnInit, OnDestroy, AfterViewInit ); } - onDismissCameraPreview() { + onDismissCameraPreview(): void { if (this.isModal) { this.modalController.dismiss(); } else { @@ -139,13 +141,13 @@ export class CaptureReceiptComponent implements OnInit, OnDestroy, AfterViewInit } } - onToggleFlashMode(flashMode: 'on' | 'off') { + onToggleFlashMode(flashMode: 'on' | 'off'): void { this.trackingService.flashModeSet({ FlashMode: flashMode, }); } - showBulkModeToastMessage() { + showBulkModeToastMessage(): void { const message = 'If you have multiple receipts to upload, please use BULK MODE to upload all the receipts at once.'; this.bulkModeToastMessageRef = this.matSnackBar.openFromComponent(ToastMessageComponent, { @@ -157,7 +159,7 @@ export class CaptureReceiptComponent implements OnInit, OnDestroy, AfterViewInit this.trackingService.showToastMessage({ ToastContent: message }); } - onSwitchMode() { + onSwitchMode(): void { this.isBulkMode = !this.isBulkMode; if (this.isBulkMode) { @@ -167,7 +169,7 @@ export class CaptureReceiptComponent implements OnInit, OnDestroy, AfterViewInit } } - onSingleCaptureOffline() { + onSingleCaptureOffline(): void { this.loaderService.showLoader(); this.addMultipleExpensesToQueue(this.base64ImagesWithSource) .pipe(finalize(() => this.loaderService.hideLoader())) @@ -176,7 +178,7 @@ export class CaptureReceiptComponent implements OnInit, OnDestroy, AfterViewInit }); } - navigateToExpenseForm() { + navigateToExpenseForm(): void { const isInstafyleEnabled$ = this.orgUserSettingsService .get() .pipe( @@ -199,7 +201,7 @@ export class CaptureReceiptComponent implements OnInit, OnDestroy, AfterViewInit }); } - saveSingleCapture() { + saveSingleCapture(): void { this.isOffline$.pipe(take(1)).subscribe((isOffline) => { if (isOffline) { this.onSingleCaptureOffline(); @@ -210,13 +212,13 @@ export class CaptureReceiptComponent implements OnInit, OnDestroy, AfterViewInit this.transactionsOutboxService.incrementSingleCaptureCount(); } - onSingleCapture() { + onSingleCapture(): void { const receiptPreviewModal = this.createReceiptPreviewModal('single'); const receiptPreviewDetails$ = from(receiptPreviewModal).pipe( shareReplay(1), tap((receiptPreviewModal) => receiptPreviewModal.present()), - switchMap((receiptPreviewModal) => receiptPreviewModal.onWillDismiss()), + switchMap((receiptPreviewModal) => receiptPreviewModal.onWillDismiss()), map((receiptPreviewData) => receiptPreviewData?.data), filter((receiptPreviewDetails) => !!receiptPreviewDetails) ); @@ -258,7 +260,7 @@ export class CaptureReceiptComponent implements OnInit, OnDestroy, AfterViewInit saveReceipt$.pipe(filter((isModal) => !isModal)).subscribe(() => this.saveSingleCapture()); } - addPerformanceTrackers() { + addPerformanceTrackers(): void { this.orgService.getOrgs().subscribe((orgs) => { const isMultiOrg = orgs.length > 1; @@ -275,7 +277,7 @@ export class CaptureReceiptComponent implements OnInit, OnDestroy, AfterViewInit const measureLaunchTime = performance.getEntriesByName(PerfTrackers.appLaunchTime); // eslint-disable-next-line @typescript-eslint/dot-notation - const isLoggedIn = performance.getEntriesByName(PerfTrackers.appLaunchStartTime)[0]['detail']; + const isLoggedIn = performance.getEntriesByName(PerfTrackers.appLaunchStartTime)[0]['detail'] as boolean; // Converting the duration to seconds and fix it to 3 decimal places const launchTimeDuration = (measureLaunchTime[0]?.duration / 1000)?.toFixed(3); @@ -289,7 +291,7 @@ export class CaptureReceiptComponent implements OnInit, OnDestroy, AfterViewInit }); } - openReceiptPreviewModal() { + openReceiptPreviewModal(): void { const receiptPreviewDetails$ = this.showReceiptPreview().pipe(filter((data) => !!data)); receiptPreviewDetails$ @@ -313,7 +315,7 @@ export class CaptureReceiptComponent implements OnInit, OnDestroy, AfterViewInit .pipe( filter( (receiptPreviewDetails) => - !receiptPreviewDetails.continueCaptureReceipt && receiptPreviewDetails.base64ImagesWithSource.length + !receiptPreviewDetails.continueCaptureReceipt && !!receiptPreviewDetails.base64ImagesWithSource.length ), switchMap(() => { this.loaderService.showLoader('Please wait...', 10000); @@ -326,7 +328,7 @@ export class CaptureReceiptComponent implements OnInit, OnDestroy, AfterViewInit }); } - createReceiptPreviewModal(mode: 'single' | 'bulk') { + createReceiptPreviewModal(mode: 'single' | 'bulk'): Promise { return this.modalController.create({ component: ReceiptPreviewComponent, componentProps: { @@ -336,19 +338,19 @@ export class CaptureReceiptComponent implements OnInit, OnDestroy, AfterViewInit }); } - showReceiptPreview() { + showReceiptPreview(): Observable { return from(this.createReceiptPreviewModal('bulk')).pipe( tap((receiptPreviewModal) => receiptPreviewModal.present()), - switchMap((receiptPreviewModal) => receiptPreviewModal.onWillDismiss()), + switchMap((receiptPreviewModal) => receiptPreviewModal.onWillDismiss()), map((receiptPreviewDetails) => receiptPreviewDetails?.data) ); } - onBulkCapture() { + onBulkCapture(): void { this.noOfReceipts += 1; } - showLimitReachedPopover() { + showLimitReachedPopover(): Observable { const limitReachedPopover = this.popoverController.create({ component: PopupAlertComponent, componentProps: { @@ -365,7 +367,7 @@ export class CaptureReceiptComponent implements OnInit, OnDestroy, AfterViewInit return from(limitReachedPopover).pipe(tap((limitReachedPopover) => limitReachedPopover.present())); } - onCaptureReceipt() { + onCaptureReceipt(): void { if (this.noOfReceipts >= 20) { this.trackingService.receiptLimitReached(); this.showLimitReachedPopover().subscribe(noop); @@ -395,7 +397,7 @@ export class CaptureReceiptComponent implements OnInit, OnDestroy, AfterViewInit } } - setupPermissionDeniedPopover(permissionType: 'CAMERA' | 'GALLERY') { + setupPermissionDeniedPopover(permissionType: 'CAMERA' | 'GALLERY'): Promise { const isIos = this.devicePlatform === 'ios'; const galleryPermissionName = isIos ? 'Photos' : 'Storage'; @@ -428,11 +430,11 @@ export class CaptureReceiptComponent implements OnInit, OnDestroy, AfterViewInit }); } - showPermissionDeniedPopover(permissionType: 'CAMERA' | 'GALLERY') { + showPermissionDeniedPopover(permissionType: 'CAMERA' | 'GALLERY'): void { from(this.setupPermissionDeniedPopover(permissionType)) .pipe( tap((permissionDeniedPopover) => permissionDeniedPopover.present()), - switchMap((permissionDeniedPopover) => permissionDeniedPopover.onWillDismiss()) + switchMap((permissionDeniedPopover) => permissionDeniedPopover.onWillDismiss<{ action: string }>()) ) .subscribe(({ data }) => { if (data?.action === 'OPEN_SETTINGS') { @@ -445,7 +447,7 @@ export class CaptureReceiptComponent implements OnInit, OnDestroy, AfterViewInit }); } - onGalleryUpload() { + onGalleryUpload(): void { this.trackingService.instafyleGalleryUploadOpened({}); const checkPermission$ = from(this.imagePicker.hasReadPermission()).pipe(shareReplay(1)); @@ -476,7 +478,7 @@ export class CaptureReceiptComponent implements OnInit, OnDestroy, AfterViewInit }); receiptsFromGallery$ - .pipe(filter((receiptsFromGallery) => receiptsFromGallery.length > 0)) + .pipe(filter((receiptsFromGallery: string[]) => receiptsFromGallery.length > 0)) .subscribe((receiptsFromGallery) => { receiptsFromGallery.forEach((receiptBase64) => { const receiptBase64Data = 'data:image/jpeg;base64,' + receiptBase64; @@ -489,24 +491,24 @@ export class CaptureReceiptComponent implements OnInit, OnDestroy, AfterViewInit }); receiptsFromGallery$ - .pipe(filter((receiptsFromGallery) => !receiptsFromGallery.length)) + .pipe(filter((receiptsFromGallery: string[]) => !receiptsFromGallery.length)) .subscribe(() => this.setUpAndStartCamera()); } - ngAfterViewInit() { + ngAfterViewInit(): void { if (this.isModal) { this.setUpAndStartCamera(); } } - ngOnDestroy() { + ngOnDestroy(): void { if (this.isModal) { this.stopCamera(); } this.bulkModeToastMessageRef?.dismiss?.(); } - setUpAndStartCamera() { + setUpAndStartCamera(): void { this.cameraPreview.setUpAndStartCamera(); if (this.transactionsOutboxService.singleCaptureCount === 3) { this.showBulkModeToastMessage(); @@ -514,7 +516,7 @@ export class CaptureReceiptComponent implements OnInit, OnDestroy, AfterViewInit } } - stopCamera() { + stopCamera(): void { this.cameraPreview.stopCamera(); } } diff --git a/src/app/shared/components/sidemenu/sidemenu.component.ts b/src/app/shared/components/sidemenu/sidemenu.component.ts index ea22bf261d..4d6db8fa7d 100644 --- a/src/app/shared/components/sidemenu/sidemenu.component.ts +++ b/src/app/shared/components/sidemenu/sidemenu.component.ts @@ -22,7 +22,6 @@ import { MenuController } from '@ionic/angular'; import { SidemenuAllowedActions } from 'src/app/core/models/sidemenu-allowed-actions.model'; import { OrgSettings } from 'src/app/core/models/org-settings.model'; - @Component({ selector: 'app-sidemenu', templateUrl: './sidemenu.component.html', @@ -98,7 +97,6 @@ export class SidemenuComponent implements OnInit { } async showSideMenuOnline(): Promise { - const isLoggedIn = await this.routerAuthService.isLoggedIn(); if (!isLoggedIn) { return; @@ -182,7 +180,6 @@ export class SidemenuComponent implements OnInit { ); } - getCardOptions(): Partial[] { const cardOptions = [ { @@ -197,7 +194,6 @@ export class SidemenuComponent implements OnInit { return cardOptions.filter((cardOption) => cardOption.isVisible); } - getTeamOptions(): Partial[] { const showTeamReportsPage = this.primaryOrg?.id === (this.activeOrg as Org)?.id;