-
Notifications
You must be signed in to change notification settings - Fork 15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: Allow save expense with invalid fields after capture receipt #3308
Changes from all commits
253d674
ca1b84b
467025e
4809082
436e0ef
469dea5
02fb34e
ce96a7f
4cf8b8f
8ee656f
32abf93
71c5167
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -41,6 +41,7 @@ import { | |
txnData, | ||
txnData2, | ||
txnData4, | ||
txnDataCleaned, | ||
txnDataPayload, | ||
txnList, | ||
upsertTxnParam, | ||
|
@@ -1100,9 +1101,14 @@ describe('TransactionService', () => { | |
const mockFileObject = cloneDeep(fileObjectData1); | ||
|
||
spyOn(transactionService, 'upsert').and.returnValue(of(txnData2)); | ||
expensesService.createFromFile.and.returnValue(of({ data: [expenseData] })); | ||
transactionService.createTxnWithFiles({ ...txnData }, of(mockFileObject)).subscribe((res) => { | ||
expect(res).toEqual(txnData2); | ||
expect(transactionService.upsert).toHaveBeenCalledOnceWith({ ...txnData, file_ids: [fileObjectData1[0].id] }); | ||
expect(expensesService.createFromFile).toHaveBeenCalledOnceWith(mockFileObject[0].id, 'MOBILE_DASHCAM_BULK'); | ||
expect(transactionService.upsert).toHaveBeenCalledOnceWith({ | ||
...txnDataCleaned, | ||
id: expenseData.id, | ||
}); | ||
Comment on lines
+1104
to
+1111
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) Mind-blowing test case enhancement, but let's make it even more powerful! The test case looks good with the new assertions, but in true Rajinikanth style, let's make it bulletproof! Consider adding error case scenarios. Add an error test case to verify the error handling: it('should handle errors when creating transaction with files', (done) => {
const mockFileObject = cloneDeep(fileObjectData1);
const error = new Error('File creation failed');
expensesService.createFromFile.and.returnValue(throwError(() => error));
transactionService.createTxnWithFiles({ ...txnData }, of(mockFileObject)).subscribe({
error: (err) => {
expect(err).toBe(error);
expect(expensesService.createFromFile).toHaveBeenCalledOnceWith(mockFileObject[0].id, 'MOBILE_DASHCAM_BULK');
expect(transactionService.upsert).not.toHaveBeenCalled();
done();
}
});
}); |
||
done(); | ||
}); | ||
}); | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -195,16 +195,22 @@ export class TransactionService { | |||||
): Observable<Partial<Transaction>> { | ||||||
return fileUploads$.pipe( | ||||||
switchMap((fileObjs) => { | ||||||
// txn contains only source key when capturing receipt | ||||||
if (txn.hasOwnProperty('source') && Object.keys(txn).length === 1) { | ||||||
const fileIds = fileObjs.map((fileObj) => fileObj.id); | ||||||
if (fileIds.length > 0) { | ||||||
return this.expensesService | ||||||
.createFromFile(fileIds[0], txn.source) | ||||||
.pipe(map((result) => this.transformExpense(result.data[0]).tx)); | ||||||
} | ||||||
const fileIds = fileObjs.map((fileObj) => fileObj.id); | ||||||
const isReceiptUpload = txn.hasOwnProperty('source') && Object.keys(txn).length === 1; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mind it! Let's make this code bulletproof, thalaiva style! Replace Here's the power-packed fix: -const isReceiptUpload = txn.hasOwnProperty('source') && Object.keys(txn).length === 1;
+const isReceiptUpload = Object.hasOwn(txn, 'source') && Object.keys(txn).length === 1; 📝 Committable suggestion
Suggested change
🧰 Tools🪛 Biome (1.9.4)[error] 199-199: Do not access Object.prototype method 'hasOwnProperty' from target object. It's recommended using Object.hasOwn() instead of using Object.hasOwnProperty(). (lint/suspicious/noPrototypeBuiltins) |
||||||
const isAmountPresent = !!txn.amount; | ||||||
if ((isReceiptUpload || !isAmountPresent) && fileIds.length > 0) { | ||||||
return this.expensesService.createFromFile(fileIds[0], txn.source).pipe( | ||||||
switchMap((result) => { | ||||||
// capture receipt flow: patching the expense in case of amount not present | ||||||
if (!isReceiptUpload && !isAmountPresent) { | ||||||
txn.id = result.data[0].id; | ||||||
return this.upsert(this.cleanupExpensePayload(txn)); | ||||||
} else { | ||||||
return of(this.transformExpense(result.data[0]).tx); | ||||||
} | ||||||
}) | ||||||
); | ||||||
} else { | ||||||
const fileIds = fileObjs.map((fileObj) => fileObj.id); | ||||||
txn.file_ids = fileIds; | ||||||
return this.upsert(txn); | ||||||
} | ||||||
|
@@ -919,4 +925,15 @@ export class TransactionService { | |||||
|
||||||
return typeOrFilter; | ||||||
} | ||||||
|
||||||
// to be used only when updating created expense with form values during capture recept flow | ||||||
private cleanupExpensePayload(txn: Partial<Transaction>): Partial<Transaction> { | ||||||
const newTxn: Partial<Transaction> = {}; | ||||||
for (const key in txn) { | ||||||
if (txn[key] !== null && txn[key] !== undefined) { | ||||||
newTxn[key] = txn[key] as Transaction[keyof Transaction]; | ||||||
} | ||||||
} | ||||||
return newTxn; | ||||||
} | ||||||
Comment on lines
+928
to
+938
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Wait a minute! Let's tighten up this method with proper types. The Here's how you can enhance it: -private cleanupExpensePayload(txn: Partial<Transaction>) {
- const newTxn = {};
+private cleanupExpensePayload(txn: Partial<Transaction>): Partial<Transaction> {
+ const newTxn: Partial<Transaction> = {};
for (const key in txn) {
- if (txn[key] !== null && txn[key] !== undefined) {
+ if (txn[key] != null) {
newTxn[key] = txn[key] as Transaction[keyof Transaction];
}
}
return newTxn;
} Alternatively, you can use private cleanupExpensePayload(txn: Partial<Transaction>): Partial<Transaction> {
return Object.fromEntries(
Object.entries(txn).filter(([_, value]) => value != null)
) as Partial<Transaction>;
}
Comment on lines
+929
to
+938
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Mind it! Let's add some punch to this code with proper types! The method needs some power-packed improvements:
Here's the superstar way to do it: - private cleanupExpensePayload(txn: Partial<Transaction>) {
- const newTxn = {};
+ private cleanupExpensePayload(txn: Partial<Transaction>): Partial<Transaction> {
+ const newTxn: Partial<Transaction> = {};
for (const key in txn) {
- if (txn[key] !== null && txn[key] !== undefined) {
+ if (txn[key] != null) {
newTxn[key] = txn[key] as Transaction[keyof Transaction];
}
}
return newTxn;
} Alternative implementation using Object.entries: private cleanupExpensePayload(txn: Partial<Transaction>): Partial<Transaction> {
return Object.fromEntries(
Object.entries(txn).filter(([_, value]) => value != null)
) as Partial<Transaction>;
} |
||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -141,14 +141,12 @@ export class TransactionsOutboxService { | |
addEntry( | ||
transaction: Partial<Transaction>, | ||
dataUrls: { url: string; type: string }[], | ||
comments?: string[], | ||
reportId?: string | ||
comments?: string[] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Mind this! Method signature updated but calls may be outdated. You've removed the |
||
): Promise<void> { | ||
this.queue.push({ | ||
transaction, | ||
dataUrls, | ||
comments, | ||
reportId, | ||
}); | ||
|
||
return this.saveQueue(); | ||
|
@@ -159,10 +157,9 @@ export class TransactionsOutboxService { | |
addEntryAndSync( | ||
transaction: Partial<Transaction>, | ||
dataUrls: { url: string; type: string }[], | ||
comments: string[], | ||
reportId: string | ||
comments: string[] | ||
): Promise<OutboxQueue> { | ||
this.addEntry(transaction, dataUrls, comments, reportId); | ||
this.addEntry(transaction, dataUrls, comments); | ||
return this.syncEntry(this.queue.pop()); | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4275,7 +4275,6 @@ export class AddEditExpensePage implements OnInit { | |
const customFields$ = this.getCustomFields(); | ||
|
||
this.trackAddExpense(); | ||
|
||
return this.generateEtxnFromFg(this.etxn$, customFields$).pipe( | ||
switchMap((etxn) => | ||
this.isConnected$.pipe( | ||
|
@@ -4361,13 +4360,12 @@ export class AddEditExpensePage implements OnInit { | |
etxn.tx.matchCCCId = this.selectedCCCTransaction.id; | ||
} | ||
|
||
let reportId: string; | ||
const formValues = this.getFormValues(); | ||
if ( | ||
formValues.report && | ||
(etxn.tx.policy_amount === null || (etxn.tx.policy_amount && !(etxn.tx.policy_amount < 0.0001))) | ||
) { | ||
reportId = formValues.report.id; | ||
etxn.tx.report_id = formValues.report.id; | ||
} | ||
|
||
etxn.dataUrls = etxn.dataUrls.map((data: FileObject) => { | ||
|
@@ -4387,8 +4385,7 @@ export class AddEditExpensePage implements OnInit { | |
this.transactionOutboxService.addEntryAndSync( | ||
etxn.tx, | ||
etxn.dataUrls as { url: string; type: string }[], | ||
comments, | ||
reportId | ||
comments | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Watch out! Incorrect parameter passed to The method Here's a suggested change: - this.transactionOutboxService.addEntryAndSync(
- etxn.tx,
- etxn.dataUrls as { url: string; type: string }[],
- comments
- );
+ this.transactionOutboxService.addEntryAndSync(
+ etxn.tx,
+ etxn.dataUrls as { url: string; type: string }[],
+ comments
+ );
|
||
) | ||
); | ||
} else { | ||
|
@@ -4404,13 +4401,12 @@ export class AddEditExpensePage implements OnInit { | |
this.transactionOutboxService.addEntryAndSync( | ||
etxn.tx, | ||
etxn.dataUrls as { url: string; type: string }[], | ||
comments, | ||
reportId | ||
comments | ||
) | ||
); | ||
} else { | ||
this.transactionOutboxService | ||
.addEntry(etxn.tx, etxn.dataUrls as { url: string; type: string }[], comments, reportId) | ||
.addEntry(etxn.tx, etxn.dataUrls as { url: string; type: string }[], comments) | ||
.then(noop); | ||
|
||
return of(null); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -247,7 +247,7 @@ export function TestCases3(getTestBed) { | |
spyOn(component, 'getCustomFields').and.returnValue(of(txnCustomPropertiesData4)); | ||
spyOn(component, 'getCalculatedDistance').and.returnValue(of('10')); | ||
component.isConnected$ = of(true); | ||
spyOn(component, 'generateEtxnFromFg').and.returnValue(of(unflattenedTxnData)); | ||
spyOn(component, 'generateEtxnFromFg').and.returnValue(of(cloneDeep(unflattenedTxnData))); | ||
spyOn(component, 'checkPolicyViolation').and.returnValue(of(expensePolicyDataWoData)); | ||
policyService.getCriticalPolicyRules.and.returnValue([]); | ||
policyService.getPolicyRules.and.returnValue([]); | ||
|
@@ -256,11 +256,18 @@ export function TestCases3(getTestBed) { | |
spyOn(component, 'getFormValues').and.returnValue({ | ||
report: expectedReportsPaginated[0], | ||
}); | ||
const expectedEtxnData = { | ||
...unflattenedTxnData, | ||
tx: { | ||
...unflattenedTxnData.tx, | ||
report_id: expectedReportsPaginated[0].id, | ||
}, | ||
}; | ||
transactionOutboxService.addEntryAndSync.and.resolveTo(outboxQueueData1[0]); | ||
fixture.detectChanges(); | ||
|
||
component.addExpense('SAVE_MILEAGE').subscribe((res) => { | ||
expect(res).toEqual(unflattenedTxnData); | ||
expect(res).toEqual(expectedEtxnData); | ||
expect(component.getCustomFields).toHaveBeenCalledTimes(1); | ||
expect(component.getCalculatedDistance).toHaveBeenCalledTimes(1); | ||
expect(component.generateEtxnFromFg).toHaveBeenCalledOnceWith( | ||
|
@@ -274,10 +281,9 @@ export function TestCases3(getTestBed) { | |
expect(component.trackCreateExpense).toHaveBeenCalledTimes(1); | ||
expect(component.getFormValues).toHaveBeenCalledTimes(1); | ||
expect(transactionOutboxService.addEntryAndSync).toHaveBeenCalledOnceWith( | ||
unflattenedTxnData.tx, | ||
expectedEtxnData.tx, | ||
unflattenedTxnData.dataUrls as any, | ||
[], | ||
expectedReportsPaginated[0].id | ||
[] | ||
); | ||
done(); | ||
}); | ||
|
@@ -318,8 +324,7 @@ export function TestCases3(getTestBed) { | |
expect(transactionOutboxService.addEntryAndSync).toHaveBeenCalledOnceWith( | ||
unflattenedMileageDataWithPolicyAmount.tx, | ||
unflattenedTxnData.dataUrls as any, | ||
[], | ||
undefined | ||
[] | ||
); | ||
done(); | ||
Comment on lines
+327
to
329
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) Don't disturb! The empty array parameter needs documentation. The empty array parameter Add a comment to clarify the purpose of the empty array: expect(transactionOutboxService.addEntryAndSync).toHaveBeenCalledOnceWith(
unflattenedMileageDataWithPolicyAmount.tx,
unflattenedTxnData.dataUrls as any,
- []
+ [] // comments array - empty when no policy violations
); Also applies to: 429-431 |
||
}); | ||
|
@@ -369,8 +374,7 @@ export function TestCases3(getTestBed) { | |
expect(transactionOutboxService.addEntryAndSync).toHaveBeenCalledOnceWith( | ||
unflattenedTxnData.tx, | ||
unflattenedTxnData.dataUrls as any, | ||
[], | ||
undefined | ||
[] | ||
); | ||
done(); | ||
}); | ||
|
@@ -422,8 +426,7 @@ export function TestCases3(getTestBed) { | |
expect(transactionOutboxService.addEntryAndSync).toHaveBeenCalledOnceWith( | ||
unflattendedTxnWithPolicyAmount.tx, | ||
unflattendedTxnWithPolicyAmount.dataUrls as any, | ||
['A comment'], | ||
undefined | ||
['A comment'] | ||
); | ||
done(); | ||
}); | ||
|
@@ -433,18 +436,25 @@ export function TestCases3(getTestBed) { | |
component.isConnected$ = of(false); | ||
spyOn(component, 'getCustomFields').and.returnValue(of(txnCustomPropertiesData4)); | ||
spyOn(component, 'getCalculatedDistance').and.returnValue(of('10')); | ||
spyOn(component, 'generateEtxnFromFg').and.returnValue(of(unflattenedTxnData)); | ||
spyOn(component, 'generateEtxnFromFg').and.returnValue(of(cloneDeep(unflattenedTxnData))); | ||
spyOn(component, 'checkPolicyViolation').and.returnValue(of(expensePolicyDataWoData)); | ||
spyOn(component, 'trackCreateExpense'); | ||
authService.getEou.and.resolveTo(apiEouRes); | ||
spyOn(component, 'getFormValues').and.returnValue({ | ||
report: expectedReportsPaginated[0], | ||
}); | ||
const expectedEtxnData = { | ||
...unflattenedTxnData, | ||
tx: { | ||
...unflattenedTxnData.tx, | ||
report_id: expectedReportsPaginated[0].id, | ||
}, | ||
}; | ||
Comment on lines
+439
to
+452
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Kabali da! The offline mode test case needs better structure. The offline mode test case duplicates the expectedEtxnData object construction. This could be moved to a shared setup. Extract the expected data setup: +let expectedOfflineEtxnData;
+beforeEach(() => {
+ expectedOfflineEtxnData = {
+ ...unflattenedTxnData,
+ tx: {
+ ...unflattenedTxnData.tx,
+ report_id: expectedReportsPaginated[0].id,
+ },
+ };
+});
it('should add expense in offline mode', (done) => {
component.isConnected$ = of(false);
spyOn(component, 'getCustomFields').and.returnValue(of(txnCustomPropertiesData4));
spyOn(component, 'getCalculatedDistance').and.returnValue(of('10'));
- spyOn(component, 'generateEtxnFromFg').and.returnValue(of(cloneDeep(unflattenedTxnData)));
+ spyOn(component, 'generateEtxnFromFg').and.returnValue(of(expectedOfflineEtxnData)); Also applies to: 457-471 |
||
transactionOutboxService.addEntryAndSync.and.resolveTo(outboxQueueData1[0]); | ||
fixture.detectChanges(); | ||
|
||
component.addExpense('SAVE_MILEAGE').subscribe((res) => { | ||
expect(res).toEqual(unflattenedTxnData); | ||
expect(res).toEqual(expectedEtxnData); | ||
expect(component.getCustomFields).toHaveBeenCalledTimes(1); | ||
expect(component.getCalculatedDistance).toHaveBeenCalledTimes(1); | ||
expect(component.generateEtxnFromFg).toHaveBeenCalledOnceWith( | ||
|
@@ -456,10 +466,9 @@ export function TestCases3(getTestBed) { | |
expect(component.trackCreateExpense).toHaveBeenCalledTimes(1); | ||
expect(component.getFormValues).toHaveBeenCalledTimes(1); | ||
expect(transactionOutboxService.addEntryAndSync).toHaveBeenCalledOnceWith( | ||
unflattenedTxnData.tx, | ||
expectedEtxnData.tx, | ||
unflattenedTxnData.dataUrls as any, | ||
[], | ||
expectedReportsPaginated[0].id | ||
[] | ||
); | ||
done(); | ||
}); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick (assertive)
Mind-blowing test case, but let's make it more robust!
The test case looks good, but in true Rajinikanth style, let's make it even more powerful! Consider adding assertions to verify that
expensesService.createFromFile
was called with the correct parameters.Add this assertion after the existing expect statements:
transactionService.createTxnWithFiles({ ...txnData }, of(mockFileObject)).subscribe((res) => { expect(res).toEqual(txnData2); expect(transactionService.upsert).toHaveBeenCalledOnceWith({ ...txnData, file_ids: [fileObjectData1[0].id] }); + expect(expensesService.createFromFile).toHaveBeenCalledOnceWith(mockFileObject[0].id, txnData.source); done(); });