Skip to content

Conversation

@arjunaj5
Copy link
Contributor

@arjunaj5 arjunaj5 commented Sep 9, 2025

Clickup

Please add link here

Code Coverage

Please add code coverage here

UI Preview

Please add screenshots for UI changes

Summary by CodeRabbit

  • New Features

    • Transactions now display linked source account info where available.
  • Refactor

    • UI and pages updated to the new transaction data model (fields, amounts, categories, dates, vendor, receipts).
    • Pending/outbox handling migrated to the transaction shape for more consistent offline flows.
  • Bug Fixes

    • More consistent draft, policy-violation, selection and deletion behavior.
    • Improved receipt attachment and currency/original-amount display.

@coderabbitai
Copy link

coderabbitai bot commented Sep 9, 2025

Walkthrough

The codebase migrates from the legacy Expense shape to v1 Transaction: deletes the Expense interface and a dependent-field mock, updates models, services, components, pages, templates, and tests to use Partial, adds Transaction.source_account, and removes legacy payment-mode helpers and related tests.

Changes

Cohort / File(s) Summary of changes
Model & mock removals
src/app/core/models/expense.model.ts, src/app/core/mock-data/dependent-field-expenses.data.ts, src/app/core/mock-data/expense.data.ts
Deleted Expense interface and dependent-field mock; removed unused Expense import in expense.data.ts.
Transaction model update
src/app/core/models/v1/transaction.model.ts
Added `source_account?: Pick<Account, 'id'
TransactionService refactor
src/app/core/services/transaction.service.ts
Switched public APIs to Partial<Transaction>, remapped tx_* fields to Transaction fields (id, state, category, amount, distance/num_days, corporate_credit_card_expense_group_id, etc.), added source_account in transform payload, removed payment-mode and currency-summary helpers.
Outbox service & spec
src/app/core/services/transactions-outbox.service.ts, src/app/core/services/transactions-outbox.service.spec.ts
Renamed getPendingTransactions()getPendingExpenses(); signatures updated to Partial<Transaction>[]; spec updated accordingly.
My Expenses page & spec
src/app/fyle/my-expenses/my-expenses.page.ts, src/app/fyle/my-expenses/my-expenses.page.html, src/app/fyle/my-expenses/my-expenses.page.spec.ts
Replaced pendingTransactionspendingExpenses, migrated types to Partial<Transaction>, removed formatTransactions, updated selection/deletion logic and template bindings to Transaction fields; tests updated.
Expenses Card component & template
src/app/shared/components/expenses-card/expenses-card.component.ts, src/app/shared/components/expenses-card/expenses-card.component.html
Inputs/outputs changed to Partial<Transaction>, bindings remapped (id, spent_at/created_at, file_ids, amount/orig_amount/currency, category.name/system_category, vendorDetails, isDraft, isSplitExpense), added public isDraft and vendorDetails.
Tests & small cleanups
src/app/core/services/custom-inputs.service.spec.ts, src/app/shared/components/review-footer/review-footer.component.spec.ts, src/app/core/services/transaction.service.spec.ts
Removed unused mock imports; tweaked Ionic import; removed payment-mode related tests from TransactionService spec.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant MyExpenses as MyExpensesPage
  participant Outbox as TransactionsOutboxService
  participant TS as TransactionService
  participant Card as ExpensesCardComponent

  User->>MyExpenses: Open My Expenses
  MyExpenses->>Outbox: getPendingExpenses()
  Outbox-->>MyExpenses: Partial<Transaction>[]
  MyExpenses->>Card: Render cards (Partial<Transaction>)

  User->>Card: Select / long-press
  Card-->>MyExpenses: cardClickedForSelection(expense: Partial<Transaction>)
  MyExpenses->>MyExpenses: Toggle selection (by id / deep compare)

  User->>MyExpenses: Delete selected
  MyExpenses->>TS: getExpenseDeletionMessage(selected)
  TS-->>MyExpenses: message
  MyExpenses->>Outbox: deleteBulkOfflineExpenses(pendingExpenses, selected)
  Outbox->>Outbox: remove by index
  Outbox-->>MyExpenses: updated pendingExpenses
  MyExpenses-->>User: updated UI
Loading
sequenceDiagram
  autonumber
  participant TS as TransactionService
  participant TX as Transaction (Partial)

  note over TS,TX: Field access refactor from tx_* → Transaction fields
  TX->>TS: getIsDraft(tx)
  TS->>TS: return tx.state === 'DRAFT'
  TS-->>TX: boolean

  TX->>TS: getVendorDetails(tx)
  TS->>TS: derive vendor from tx.vendor / tx.vendorDetails
  TS-->>TX: string
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • Chethan-Fyle
  • mvaishnavi

Pre-merge checks (2 passed, 1 warning)

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description Check ⚠️ Warning This pull request description is like an empty stage before the superstar arrives, containing only placeholder text for the ClickUp link, code coverage, and UI preview without any real content. Reviewers are left in the dark about the task’s context, coverage impact, and visual changes due to these missing details. Invite the superstar details by replacing each placeholder with the actual ClickUp ticket URL, precise code coverage metrics or a coverage report, and relevant UI screenshots or videos showcasing any interface updates.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title punches like a bullet, shining the spotlight on the removal of the unwanted Expense model with tx_ properties. It directly summarizes the key transformation of the codebase without dragging in every detail, giving the team instant clarity. Such razor-sharp focus ensures teammates scanning PR history will grasp the main change in a glance.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

Poem

I am Rajini — code flips like a cape,
Expense retires, Transaction takes shape.
Fields realigned, outbox marches on,
Tests nod, templates sing a new dawn.
Source_account enters — mic drop, curtain drawn. ✨

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
		  - name: "Undocumented Breaking Changes"
			  mode: "warning"
			  instructions: |
				  Flag potential breaking changes that are not documented:
				  1. Identify changes to public APIs/exports, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints (including removed/renamed items and changes to types, required params, return values, defaults, or behavior).
				  2. Ignore purely internal/private changes (e.g., code not exported from package entry points or marked internal).
				  3. Verify documentation exists: a "Breaking Change" section in the PR description and updates to CHANGELOG.md.

Please share your feedback with us on this Discord post.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch Fyle-expense_tx_model-clean

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot added the size/XL Extra Large PR label Sep 9, 2025
@arjunaj5 arjunaj5 requested a review from mvaishnavi September 9, 2025 05:32
@github-actions
Copy link

github-actions bot commented Sep 9, 2025

PR description must contain a link to a ClickUp (case-insensitive)

@if (!(filters?.state || filters?.date || simpleSearchText) && !isLoading) {
<div>
@for (pendingTransaction of pendingTransactions; track pendingTransaction.id; let i = $index) {
@for (pendingTransaction of pendingTransactions; track pendingTransaction; let i = $index) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

id does not exist for pending expenses

@github-actions
Copy link

github-actions bot commented Sep 9, 2025

PR description must contain a link to a ClickUp (case-insensitive)

@github-actions
Copy link

github-actions bot commented Sep 9, 2025

PR description must contain a link to a ClickUp (case-insensitive)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (9)
src/app/fyle/my-expenses/my-expenses.page.html (1)

224-237: Track by a stable key, not object identity.

Tracking the whole object will re-render on every refresh since outbox items are re-materialized. Track by index for offline list. One shot, perfect shot.

-@for (pendingTransaction of pendingTransactions; track pendingTransaction; let i = $index) {
+@for (pendingTransaction of pendingTransactions; track $index; let i = $index) {
src/app/core/services/transactions-outbox.service.ts (1)

198-205: Beware negative indexes in deleteBulkOfflineExpenses
Filter out -1 values before deletion or you will blast the wrong queue item, mind it!

-  deleteBulkOfflineExpenses(pendingTransactions: Partial<Transaction>[], deleteExpenses: Partial<Transaction>[]): void {
-    const indexes = deleteExpenses.map((offlineExpense) => indexOf(pendingTransactions, offlineExpense));
+  deleteBulkOfflineExpenses(pendingTransactions: Partial<Transaction>[], deleteExpenses: Partial<Transaction>[]): void {
+    const indexes = deleteExpenses
+      .map((offlineExpense) => indexOf(pendingTransactions, offlineExpense))
+      .filter((i) => i > -1);

Also update the unit test in transactions-outbox.service.spec.ts to remove the expectation for deleteOfflineExpense(-1) and adjust the call count accordingly.

src/app/fyle/my-expenses/my-expenses.page.ts (2)

902-919: Call sync once, not N times.

from(this.pendingTransactions).pipe(switchMap(() => sync())) starts multiple syncs and cancels mid-flight; unnecessary and brittle. Keep it simple, keep it stylish.

   syncOutboxExpenses(): void {
-    this.pendingTransactions = this.transactionOutboxService.getPendingTransactions();
-    if (this.pendingTransactions.length > 0) {
-      this.syncing = true;
-      from(this.pendingTransactions)
-        .pipe(
-          switchMap(() => from(this.transactionOutboxService.sync())),
-          finalize(() => {
-            this.syncing = false;
-            const pendingTransactions = this.transactionOutboxService.getPendingTransactions();
-            if (pendingTransactions.length === 0) {
-              this.doRefresh();
-              this.pendingTransactions = [];
-            }
-          }),
-        )
-        .subscribe(noop);
-    }
+    this.pendingTransactions = this.transactionOutboxService.getPendingTransactions();
+    if (this.pendingTransactions.length > 0) {
+      this.syncing = true;
+      from(this.transactionOutboxService.sync())
+        .pipe(
+          finalize(() => {
+            this.syncing = false;
+            const pendingTransactions = this.transactionOutboxService.getPendingTransactions();
+            if (pendingTransactions.length === 0) {
+              this.doRefresh();
+              this.pendingTransactions = [];
+            }
+          }),
+        )
+        .subscribe(noop);
+    }
   }

1639-1651: Bulk delete flow OK, but return type is misleading.

Offline path returns of(null) though the method is Observable<void>. Minor, but tighten it.

-  deleteSelectedExpenses(offlineExpenses: Partial<Transaction>[]): Observable<void> {
+  deleteSelectedExpenses(offlineExpenses: Partial<Transaction>[]): Observable<void> {
     if (offlineExpenses?.length > 0) {
       this.transactionOutboxService.deleteBulkOfflineExpenses(this.pendingTransactions, offlineExpenses);
-      return of(null);
+      return of(void 0);
     } else {
       ...
     }
   }
src/app/shared/components/expenses-card/expenses-card.component.html (2)

47-49: Typo in isSycing$ — rename to isSyncing$ for sanity. Style-aa irukku, clarity-um venum.

Minor, but improves readability and greppability. Update TS and template together.

Also applies to: 108-110


106-106: CSS class typo: deatils → details.

Tiny nit; fix if CSS allows.

src/app/shared/components/expenses-card/expenses-card.component.ts (2)

184-191: Update getter to read InputSignal. Semma smooth.

   get isSelected(): boolean {
-    if (this.selectedElements) {
-      if (this.expense.id) {
-        return this.selectedElements.some((txn) => this.expense.id === txn.id);
+    const selected = this.selectedElements();
+    if (selected) {
+      if (this.expense?.id) {
+        return selected.some((txn) => this.expense.id === txn.id);
       } else {
-        return this.selectedElements.some((txn) => isEqual(this.expense, txn));
+        return selected.some((txn) => isEqual(this.expense, txn));
       }
     }
     return false;
   }

201-215: Receipt indicator logic: empty array treated as truthy — fix check, thalaiva.

[] is truthy; current code marks receipt present even when none.

-    } else {
-      if (!this.expense.file_ids) {
+    } else {
+      if (!this.expense.file_ids || this.expense.file_ids.length === 0) {
         this.receiptIcon = 'assets/svg/list-plus.svg';
         if (this.isFromPotentialDuplicates || this.isFromViewReports) {
           this.receiptIcon = 'assets/svg/list.svg';
         }
       } else {
         this.isReceiptPresent = true;
       }
     }
src/app/core/services/transaction.service.ts (1)

293-310: Per diem vendor string can crash when num_days is undefined — guard it. Nallaa handle pannunga.

-    } else if (systemCategory === 'per diem') {
-      vendorDisplayName = expense.num_days.toString();
-      if (expense.num_days > 1) {
+    } else if (systemCategory === 'per diem') {
+      const days = expense.num_days ?? 0;
+      vendorDisplayName = days.toString();
+      if (days > 1) {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 06c9eae and 536b1d3.

📒 Files selected for processing (13)
  • src/app/core/mock-data/dependent-field-expenses.data.ts (0 hunks)
  • src/app/core/mock-data/expense.data.ts (0 hunks)
  • src/app/core/models/expense.model.ts (0 hunks)
  • src/app/core/models/v1/transaction.model.ts (2 hunks)
  • src/app/core/services/custom-inputs.service.spec.ts (0 hunks)
  • src/app/core/services/transaction.service.spec.ts (0 hunks)
  • src/app/core/services/transaction.service.ts (6 hunks)
  • src/app/core/services/transactions-outbox.service.ts (1 hunks)
  • src/app/fyle/my-expenses/my-expenses.page.html (1 hunks)
  • src/app/fyle/my-expenses/my-expenses.page.ts (7 hunks)
  • src/app/shared/components/expenses-card/expenses-card.component.html (8 hunks)
  • src/app/shared/components/expenses-card/expenses-card.component.ts (16 hunks)
  • src/app/shared/components/review-footer/review-footer.component.spec.ts (1 hunks)
💤 Files with no reviewable changes (5)
  • src/app/core/services/custom-inputs.service.spec.ts
  • src/app/core/services/transaction.service.spec.ts
  • src/app/core/mock-data/expense.data.ts
  • src/app/core/models/expense.model.ts
  • src/app/core/mock-data/dependent-field-expenses.data.ts
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{service,pipe,directive}.ts

📄 CodeRabbit inference engine (.cursor/rules/atomics-i18n-key-pattern.mdc)

**/*.{service,pipe,directive}.ts: i18n keys for services, pipes, and directives must use the schema: services.., pipes.., directives..
Convert kebab-case file names to camelCase and remove the suffix (Service, Pipe, Directive) when generating i18n keys for services, pipes, and directives
Use only the three roots: services, pipes, directives for i18n key grouping
Use meaningful, semantic, and context-aware key names for i18n keys; avoid generic names like message1, text1, error1

Files:

  • src/app/core/services/transactions-outbox.service.ts
  • src/app/core/services/transaction.service.ts
**/*.component.{ts,html}

📄 CodeRabbit inference engine (.cursor/rules/component-i18n-key-naming.mdc)

**/*.component.{ts,html}: Top-level object in i18n translation files must be the component or feature folder name, derived from the file name by converting kebab-case to camelCase and removing the first matching prefix from: 'feature', 'ui', 'component'.
Keys inside the top-level object in i18n translation files must be free-form, not prefixed, and there must be no nested objects beyond the first level.
Use meaningful, semantic, and context-aware key names for i18n keys; avoid generic names like 'label1', 'text1', or 'message1'.
Do not translate strings that are already translation keys, tracking/analytics strings, strings with only special characters, comments, non-user-visible text, or variable bindings and expressions.
One object per component or feature in i18n translation files; avoid deep nesting and avoid prefixes like 'ui*' or 'feature*' in object names.

Files:

  • src/app/shared/components/expenses-card/expenses-card.component.html
  • src/app/shared/components/expenses-card/expenses-card.component.ts
src/**/*.component.{ts,html}

📄 CodeRabbit inference engine (.cursor/rules/component-i18n-key-naming.mdc)

Translation keys for Angular components and templates located in 'src/**' must be placed in 'src/assets/i18n/{lang}.json' for every supported language.

Files:

  • src/app/shared/components/expenses-card/expenses-card.component.html
  • src/app/shared/components/expenses-card/expenses-card.component.ts
**/*.component.html

📄 CodeRabbit inference engine (.cursor/rules/component-i18n-key-naming.mdc)

Prefer property binding for user-facing attributes in Angular templates when using translation keys.

Files:

  • src/app/shared/components/expenses-card/expenses-card.component.html
🧠 Learnings (4)
📚 Learning: 2024-11-18T06:55:17.972Z
Learnt from: sumrender
PR: fylein/fyle-mobile-app#3265
File: src/app/fyle/personal-cards-matched-expenses/expense-preview/expense-preview.component.spec.ts:35-37
Timestamp: 2024-11-18T06:55:17.972Z
Learning: In `src/app/fyle/add-edit-expense/add-edit-expense.page.ts`, the `unmatchExpense` method is used for corporate card expenses, not for personal card expenses.

Applied to files:

  • src/app/fyle/my-expenses/my-expenses.page.html
  • src/app/shared/components/expenses-card/expenses-card.component.html
  • src/app/fyle/my-expenses/my-expenses.page.ts
  • src/app/shared/components/expenses-card/expenses-card.component.ts
  • src/app/core/services/transaction.service.ts
📚 Learning: 2024-10-23T11:45:16.974Z
Learnt from: bistaastha
PR: fylein/fyle-mobile-app#3240
File: src/app/fyle/manage-corporate-cards/manage-corporate-cards.page.spec.ts:293-354
Timestamp: 2024-10-23T11:45:16.974Z
Learning: In 'src/app/fyle/manage-corporate-cards/manage-corporate-cards.page.spec.ts', the flags 'isVisaRTFEnabled$', 'isMastercardRTFEnabled$', and 'isYodleeEnabled$' do not have rapidly changing values, so tests for rapid changes in these flags are unnecessary.

Applied to files:

  • src/app/shared/components/review-footer/review-footer.component.spec.ts
📚 Learning: 2024-11-04T13:10:36.868Z
Learnt from: arjunaj5
PR: fylein/fyle-mobile-app#3242
File: src/app/fyle/my-expenses/my-expenses.page.spec.ts:967-968
Timestamp: 2024-11-04T13:10:36.868Z
Learning: In `src/app/fyle/my-expenses/my-expenses.page.spec.ts`, when testing private methods like `filterDEIncompleteExpenses`, it's acceptable to use `//ts-ignore` to spy on them in unit tests.

Applied to files:

  • src/app/shared/components/review-footer/review-footer.component.spec.ts
📚 Learning: 2024-10-23T08:40:59.542Z
Learnt from: bistaastha
PR: fylein/fyle-mobile-app#3240
File: src/app/fyle/manage-corporate-cards/manage-corporate-cards.page.spec.ts:200-200
Timestamp: 2024-10-23T08:40:59.542Z
Learning: In 'src/app/fyle/manage-corporate-cards/manage-corporate-cards.page.spec.ts', moving 'component.isAddCorporateCardVisible$ = of(true);' to the top-level beforeEach block causes tests to fail, so mock values need to be set in individual tests.

Applied to files:

  • src/app/shared/components/review-footer/review-footer.component.spec.ts
🧬 Code graph analysis (5)
src/app/core/services/transactions-outbox.service.ts (1)
src/app/core/models/v1/transaction.model.ts (1)
  • Transaction (8-142)
src/app/core/models/v1/transaction.model.ts (1)
src/app/core/models/platform/v1/account.model.ts (1)
  • Account (1-12)
src/app/fyle/my-expenses/my-expenses.page.ts (1)
src/app/core/models/v1/transaction.model.ts (1)
  • Transaction (8-142)
src/app/shared/components/expenses-card/expenses-card.component.ts (1)
src/app/core/models/v1/transaction.model.ts (1)
  • Transaction (8-142)
src/app/core/services/transaction.service.ts (1)
src/app/core/models/v1/transaction.model.ts (1)
  • Transaction (8-142)
🪛 GitHub Check: Run linters
src/app/shared/components/expenses-card/expenses-card.component.ts

[failure] 76-76:
Use InputSignals (e.g. via input()) for Component input properties rather than the legacy @Input() decorator


[failure] 92-92:
Use InputSignals (e.g. via input()) for Component input properties rather than the legacy @Input() decorator


[failure] 151-151:
Expected blank line between class members


[failure] 150-150:
Expected blank line between class members

🪛 GitHub Actions: Lint
src/app/shared/components/expenses-card/expenses-card.component.ts

[error] 76-76: lint:check failed: ESLint: Use InputSignals (e.g. via input()) for Component input properties rather than the legacy @Input() decorator. (@angular-eslint/prefer-signals)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (8)
src/app/shared/components/review-footer/review-footer.component.spec.ts (2)

3-3: Import cleanup LGTM.

Removing IonButton import is correct since the spec doesn’t reference it.


108-119: Replace stale mock with apiExpenseRes in review-footer spec
Replace deleted expensesWithDependentFields with apiExpenseRes and set activeIndex to apiExpenseRes.length - 1. Tests will rock like a Rajini punch!

-    reviewFooterComponent.activeIndex = 1;
-    reviewFooterComponent.reviewList = expensesWithDependentFields;
+    reviewFooterComponent.reviewList = apiExpenseRes;
+    reviewFooterComponent.activeIndex = apiExpenseRes.length - 1;
src/app/core/models/v1/transaction.model.ts (1)

4-4: LGTM: source_account added with correct type.

Import path is right; Pick<Account, 'id' | 'type'> is precise. Superstar move.

Also applies to: 71-71

src/app/fyle/my-expenses/my-expenses.page.ts (1)

134-134: LGTM: type migration to Partial.

Consistent with the model refactor. Rock solid.

Also applies to: 140-140, 195-195

src/app/shared/components/expenses-card/expenses-card.component.ts (1)

76-76: Verify mutation of expense and selectedElements migration

  • expense: Partial<Transaction> is mutated (e.g. updating file_ids), so either keep the @Input() with
    // eslint-disable-next-line @angular-eslint/prefer-signals
    @Input() expense: Partial<Transaction>;
    or refactor to an immutable pattern before adopting signals.
  • selectedElements: Partial<Transaction>[] is read-only — safe to convert to an InputSignal, for example:
    readonly selectedElements = input<Partial<Transaction>[]>([]);

Confirm in your component that expense is indeed mutated and adjust accordingly, ha!

src/app/core/services/transaction.service.ts (3)

322-333: State helpers OK — but consider strict equality with nullish coalescing for clarity. Rock solid already.


589-596: source_account passthrough LGTM.

Mapping aligns with Transaction model usage in the component.


636-662: Confirm backend contract for tx_ filters*
Frontend still uses tx_* query params (tx_report_id, tx_state, tx_policy_amount via FilterQueryParams and GetExpensesQueryParams). Ensure backend API still accepts these after the refactor—no surprises, superstar!

Comment on lines +413 to 423
isMergeAllowed(expenses: Partial<Transaction>[]): boolean {
if (expenses.length === 2) {
const areSomeMileageOrPerDiemExpenses = expenses.some(
(expense) => expense.tx_fyle_category === 'Mileage' || expense.tx_fyle_category === 'Per Diem',
(expense) => expense.category?.system_category === 'Mileage' || expense.category?.system_category === 'Per Diem',
);
const areAllExpensesSubmitted = expenses.every((expense) =>
['APPROVER_PENDING', 'APPROVED', 'PAYMENT_PENDING', 'PAYMENT_PROCESSING', 'PAID'].includes(expense.tx_state),
['APPROVER_PENDING', 'APPROVED', 'PAYMENT_PENDING', 'PAYMENT_PROCESSING', 'PAID'].includes(expense.state),
);
const areAllCCCMatchedExpenses = expenses.every((expense) => expense.tx_corporate_credit_card_expense_group_id);
const areAllCCCMatchedExpenses = expenses.every((expense) => expense.corporate_credit_card_expense_group_id);
return !areSomeMileageOrPerDiemExpenses && !areAllExpensesSubmitted && !areAllCCCMatchedExpenses;
} else {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Merge rules look tight. One small ask: guard state undefined for includes().

Minor, optional.

-      const areAllExpensesSubmitted = expenses.every((expense) =>
-        ['APPROVER_PENDING', 'APPROVED', 'PAYMENT_PENDING', 'PAYMENT_PROCESSING', 'PAID'].includes(expense.state),
-      );
+      const submittedStates = ['APPROVER_PENDING', 'APPROVED', 'PAYMENT_PENDING', 'PAYMENT_PROCESSING', 'PAID'] as const;
+      const areAllExpensesSubmitted = expenses.every((expense) => !!expense.state && submittedStates.includes(expense.state));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
isMergeAllowed(expenses: Partial<Transaction>[]): boolean {
if (expenses.length === 2) {
const areSomeMileageOrPerDiemExpenses = expenses.some(
(expense) => expense.tx_fyle_category === 'Mileage' || expense.tx_fyle_category === 'Per Diem',
(expense) => expense.category?.system_category === 'Mileage' || expense.category?.system_category === 'Per Diem',
);
const areAllExpensesSubmitted = expenses.every((expense) =>
['APPROVER_PENDING', 'APPROVED', 'PAYMENT_PENDING', 'PAYMENT_PROCESSING', 'PAID'].includes(expense.tx_state),
['APPROVER_PENDING', 'APPROVED', 'PAYMENT_PENDING', 'PAYMENT_PROCESSING', 'PAID'].includes(expense.state),
);
const areAllCCCMatchedExpenses = expenses.every((expense) => expense.tx_corporate_credit_card_expense_group_id);
const areAllCCCMatchedExpenses = expenses.every((expense) => expense.corporate_credit_card_expense_group_id);
return !areSomeMileageOrPerDiemExpenses && !areAllExpensesSubmitted && !areAllCCCMatchedExpenses;
} else {
isMergeAllowed(expenses: Partial<Transaction>[]): boolean {
if (expenses.length === 2) {
const areSomeMileageOrPerDiemExpenses = expenses.some(
(expense) =>
expense.category?.system_category === 'Mileage' ||
expense.category?.system_category === 'Per Diem',
);
const submittedStates = [
'APPROVER_PENDING',
'APPROVED',
'PAYMENT_PENDING',
'PAYMENT_PROCESSING',
'PAID',
] as const;
const areAllExpensesSubmitted = expenses.every(
(expense) => !!expense.state && submittedStates.includes(expense.state),
);
const areAllCCCMatchedExpenses = expenses.every(
(expense) => expense.corporate_credit_card_expense_group_id,
);
return (
!areSomeMileageOrPerDiemExpenses &&
!areAllExpensesSubmitted &&
!areAllCCCMatchedExpenses
);
} else {
// ...
🤖 Prompt for AI Agents
In src/app/core/services/transaction.service.ts around lines 413 to 423, the
expenses.every(...) call uses includes(expense.state) without guarding for
undefined state; update that predicate to handle missing or undefined state
(e.g., check expense.state != null && ['APPROVER_PENDING',
...].includes(expense.state) or use ['...'].includes(expense.state ?? '') ) so
includes always receives a string and avoids runtime errors; make this one-line
change and keep the rest of the logic intact.

Comment on lines 1097 to 1102
setOutboxExpenseStatsOnSelect(): void {
this.allExpensesStats$ = of({
count: this.selectedOutboxExpenses.length,
amount: this.selectedOutboxExpenses.reduce((acc, txnObj) => acc + txnObj.tx_amount, 0),
amount: this.selectedOutboxExpenses.reduce((acc, exp) => acc + exp.amount, 0),
});
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Null-safe sum for optional amount.

amount is optional; 0 + undefined => NaN. Add a fallback, keep the swagger.

-    this.allExpensesStats$ = of({
-      count: this.selectedOutboxExpenses.length,
-      amount: this.selectedOutboxExpenses.reduce((acc, exp) => acc + exp.amount, 0),
-    });
+    this.allExpensesStats$ = of({
+      count: this.selectedOutboxExpenses.length,
+      amount: this.selectedOutboxExpenses.reduce((acc, exp) => acc + (exp.amount ?? 0), 0),
+    });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
setOutboxExpenseStatsOnSelect(): void {
this.allExpensesStats$ = of({
count: this.selectedOutboxExpenses.length,
amount: this.selectedOutboxExpenses.reduce((acc, txnObj) => acc + txnObj.tx_amount, 0),
amount: this.selectedOutboxExpenses.reduce((acc, exp) => acc + exp.amount, 0),
});
}
setOutboxExpenseStatsOnSelect(): void {
this.allExpensesStats$ = of({
count: this.selectedOutboxExpenses.length,
amount: this.selectedOutboxExpenses.reduce(
(acc, exp) => acc + (exp.amount ?? 0),
0
),
});
}
🤖 Prompt for AI Agents
In src/app/fyle/my-expenses/my-expenses.page.ts around lines 1097 to 1102, the
reducer sums optional expense.amount values which can be undefined and yield
NaN; update the reduce call to treat missing amounts as 0 (for example use acc +
(exp.amount ?? 0)) while keeping the existing swagger typing and initial
accumulator as 0 so the sum remains null-safe.

Comment on lines 1105 to 1117
let isSelectedElementsIncludesExpense = false;
if (expense.tx_id) {
isSelectedElementsIncludesExpense = this.selectedOutboxExpenses.some((txn) => expense.tx_id === txn.tx_id);
if (expense.id) {
isSelectedElementsIncludesExpense = this.selectedOutboxExpenses.some((exp) => expense.id === exp.id);
} else {
isSelectedElementsIncludesExpense = this.selectedOutboxExpenses.some((txn) => isEqual(txn, expense));
isSelectedElementsIncludesExpense = this.selectedOutboxExpenses.some((exp) => isEqual(exp, expense));
}

if (isSelectedElementsIncludesExpense) {
if (expense.tx_id) {
this.selectedOutboxExpenses = this.selectedOutboxExpenses.filter((txn) => txn.tx_id !== expense.tx_id);
if (expense.id) {
this.selectedOutboxExpenses = this.selectedOutboxExpenses.filter((exp) => exp.id !== expense.id);
} else {
this.selectedOutboxExpenses = this.selectedOutboxExpenses.filter((txn) => !isEqual(txn, expense));
this.selectedOutboxExpenses = this.selectedOutboxExpenses.filter((exp) => !isEqual(exp, expense));
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Selection by deep-equal is O(n²) and brittle; prefer a stable local id.

Offline txns lack id, so isEqual works but is heavy and fragile if objects rehydrate. Add a stable local_id to outbox entries and use it here. Mass save, mass style.

In TransactionsOutboxService.addEntry (separate file), set a local id:

-  addEntry(
-    transaction: Partial<Transaction>,
+  addEntry(
+    transaction: Partial<Transaction>,
     dataUrls: { url: string; type: string }[],
     comments?: string[],
   ): Promise<void> {
-    this.queue.push({
+    const localId = (crypto as any)?.randomUUID?.() ?? Date.now().toString();
+    this.queue.push({
-      transaction,
+      transaction: { ...transaction, local_id: localId } as any,
       dataUrls,
       comments,
     });
     return this.saveQueue();
   }

Then here:

-    if (expense.id) {
+    if (expense.id ?? (expense as any).local_id) {
-      isSelectedElementsIncludesExpense = this.selectedOutboxExpenses.some((exp) => expense.id === exp.id);
+      const key = expense.id ?? (expense as any).local_id;
+      isSelectedElementsIncludesExpense = this.selectedOutboxExpenses.some(
+        (exp) => (exp.id ?? (exp as any).local_id) === key,
+      );
     } else {
       isSelectedElementsIncludesExpense = this.selectedOutboxExpenses.some((exp) => isEqual(exp, expense));
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let isSelectedElementsIncludesExpense = false;
if (expense.tx_id) {
isSelectedElementsIncludesExpense = this.selectedOutboxExpenses.some((txn) => expense.tx_id === txn.tx_id);
if (expense.id) {
isSelectedElementsIncludesExpense = this.selectedOutboxExpenses.some((exp) => expense.id === exp.id);
} else {
isSelectedElementsIncludesExpense = this.selectedOutboxExpenses.some((txn) => isEqual(txn, expense));
isSelectedElementsIncludesExpense = this.selectedOutboxExpenses.some((exp) => isEqual(exp, expense));
}
if (isSelectedElementsIncludesExpense) {
if (expense.tx_id) {
this.selectedOutboxExpenses = this.selectedOutboxExpenses.filter((txn) => txn.tx_id !== expense.tx_id);
if (expense.id) {
this.selectedOutboxExpenses = this.selectedOutboxExpenses.filter((exp) => exp.id !== expense.id);
} else {
this.selectedOutboxExpenses = this.selectedOutboxExpenses.filter((txn) => !isEqual(txn, expense));
this.selectedOutboxExpenses = this.selectedOutboxExpenses.filter((exp) => !isEqual(exp, expense));
}
let isSelectedElementsIncludesExpense = false;
- if (expense.id) {
if (expense.id ?? (expense as any).local_id) {
const key = expense.id ?? (expense as any).local_id;
isSelectedElementsIncludesExpense = this.selectedOutboxExpenses.some(
(exp) => (exp.id ?? (exp as any).local_id) === key,
);
} else {
isSelectedElementsIncludesExpense = this.selectedOutboxExpenses.some((exp) => isEqual(exp, expense));
}
if (isSelectedElementsIncludesExpense) {
if (expense.id) {
this.selectedOutboxExpenses = this.selectedOutboxExpenses.filter((exp) => exp.id !== expense.id);
} else {
this.selectedOutboxExpenses = this.selectedOutboxExpenses.filter((exp) => !isEqual(exp, expense));
}
🤖 Prompt for AI Agents
In src/app/fyle/my-expenses/my-expenses.page.ts around lines 1105 to 1117,
replace the deep-equal based selection logic with stable local_id comparison:
check expense.local_id (fall back to id if needed) against
selectedOutboxExpenses' local_id (or id) when testing inclusion, and filter by
that same local_id when removing; assume TransactionsOutboxService.addEntry now
sets a local_id on outbox entries so use local_id for offline entries to avoid
O(n²) isEqual calls and brittle rehydration issues.

Comment on lines +4 to 6
@if (expense.id && data.isConnected) {
{{ expense.spent_at || expense.created_at | dateFormat }}
} @else {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Date pipe precedence bug — wrap with parentheses, boss.

Without parentheses, dateFormat applies only to created_at. You want it on the coalesced value.

-        {{ expense.spent_at || expense.created_at | dateFormat }}
+        {{ (expense.spent_at || expense.created_at) | dateFormat }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@if (expense.id && data.isConnected) {
{{ expense.spent_at || expense.created_at | dateFormat }}
} @else {
@if (expense.id && data.isConnected) {
{{ (expense.spent_at || expense.created_at) | dateFormat }}
} @else {
🤖 Prompt for AI Agents
In src/app/shared/components/expenses-card/expenses-card.component.html around
lines 4 to 6, the dateFormat pipe is currently only applied to created_at due to
operator precedence; wrap the coalesced expression in parentheses so the pipe
receives the entire value (expense.spent_at || expense.created_at) and not just
created_at, ensuring the selected date is formatted correctly.

Comment on lines +52 to 55
expense.dataUrls?.length > 0 &&
imageTransperencyOverlay + 'url(' + '../../../../assets/images/pdf-receipt-placeholder.png' + ')',
}"
>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Using non-existent expense.dataUrls — switch to file_ids or receipt flags. Kaboom avoided.

Transaction model doesn’t expose dataUrls. These checks will always be falsy and break UI.

-                  expense.dataUrls?.length > 0 &&
+                  (expense.file_ids?.length ?? 0) > 0 &&
-                  expense.dataUrls?.length > 0 &&
+                  (expense.file_ids?.length ?? 0) > 0 &&
-              @if (!expense.dataUrls?.length) {
+              @if (!(expense.file_ids?.length)) {

Also applies to: 65-69, 69-71

Comment on lines +251 to 252
that.isScanInProgress = !that.isScanCompleted && !this.expense.extracted_data;
} else {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

this vs that inconsistency — stick to this. Clean like a vintage entry.

-          that.isScanInProgress = !that.isScanCompleted && !this.expense.extracted_data;
+          that.isScanInProgress = !that.isScanCompleted && !that.expense.extracted_data;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
that.isScanInProgress = !that.isScanCompleted && !this.expense.extracted_data;
} else {
that.isScanInProgress = !that.isScanCompleted && !that.expense.extracted_data;
} else {
🤖 Prompt for AI Agents
In src/app/shared/components/expenses-card/expenses-card.component.ts around
lines 251-252, there's an inconsistent use of the legacy alias "that" alongside
"this"; change the code to use "this.isScanInProgress" instead of
"that.isScanInProgress" and remove the "that" alias; if the surrounding callback
is a regular function causing "this" to be lost, convert the function to an
arrow function (or explicitly bind the correct context) so "this" refers to the
component instance, then update any other occurrences of "that" in the same
scope and run lint/tests to confirm no regressions.

Comment on lines 260 to 263
canShowPaymentModeIcon(): void {
this.showPaymentModeIcon =
this.expense.source_account_type === AccountType.PERSONAL && !this.expense.tx_skip_reimbursement;
this.expense.source_account?.type === AccountType.PERSONAL_CASH_ACCOUNT && !this.expense.skip_reimbursement;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

AccountType source mismatch — verify enum and values; might be wrong constant. Mass check pannunga.

Component imports AccountType from platform/v1/account.model, service uses ../enums/account-type.enum. Also constants PERSONAL_CASH_ACCOUNT and PERSONAL_CORPORATE_CREDIT_CARD_ACCOUNT need confirmation.

If mismatch, import AccountType from the single canonical source and use the correct members.

Also applies to: 320-333


🏁 Script executed:

#!/bin/bash
# Verify AccountType definitions and usages
rg -nP --type=ts -C2 "export enum AccountType|type\s*:\s*['\"]?(PERSONAL|PERSONAL_CASH_ACCOUNT|PERSONAL_CORPORATE_CREDIT_CARD_ACCOUNT)['\"]?" src/app | sed -n '1,200p'
rg -nP --type=ts -C2 "\bAccountType\b" src/app/core | sed -n '1,200p'

Length of output: 28231


Unify AccountType import and correct enum members

  • In expenses-card.component.ts, import AccountType from src/app/core/enums/account-type.enum.ts (not platform/v1/account.model.ts).
  • Replace AccountType.PERSONAL_CASH_ACCOUNT with AccountType.PERSONAL and AccountType.PERSONAL_CORPORATE_CREDIT_CARD_ACCOUNT with AccountType.CCC (or the matching keys in the unified enum).
  • Remove the duplicate AccountType definition in platform/v1/account.model.ts and ensure all modules reference the single canonical enum.
🤖 Prompt for AI Agents
In src/app/shared/components/expenses-card/expenses-card.component.ts around
lines 260 to 263, the component currently uses a non-canonical AccountType and
incorrect enum members; update the import to pull AccountType from
src/app/core/enums/account-type.enum.ts instead of platform/v1/account.model.ts,
replace AccountType.PERSONAL_CASH_ACCOUNT with AccountType.PERSONAL (and any
AccountType.PERSONAL_CORPORATE_CREDIT_CARD_ACCOUNT usages with AccountType.CCC
or the matching key in the core enum), and then remove the duplicate AccountType
definition from platform/v1/account.model.ts and update any other modules still
importing the duplicate to reference the single canonical enum.

Comment on lines +273 to +277
this.isMileageExpense = this.expense.category?.name?.toLowerCase() === 'mileage';
this.isPerDiem = this.expense.category?.name?.toLowerCase() === 'per diem';

this.category = this.expense.tx_org_category?.toLowerCase();
this.expense.isDraft = this.transactionService.getIsDraft(this.expense);
this.expense.isPolicyViolated = this.expense.tx_policy_flag;
this.expense.isCriticalPolicyViolated = this.transactionService.getIsCriticalPolicyViolated(this.expense);
this.expense.vendorDetails = this.transactionService.getVendorDetails(this.expense);
this.category = this.expense.category?.name?.toLowerCase();
this.isDraft = this.transactionService.getIsDraft(this.expense);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Mileage/Per Diem detection should use system_category, not name. Get the category right, superstar.

Name is user-defined; system_category is canonical.

-    this.isMileageExpense = this.expense.category?.name?.toLowerCase() === 'mileage';
-    this.isPerDiem = this.expense.category?.name?.toLowerCase() === 'per diem';
-    this.category = this.expense.category?.name?.toLowerCase();
+    this.isMileageExpense = this.expense.category?.system_category === 'Mileage';
+    this.isPerDiem = this.expense.category?.system_category === 'Per Diem';
+    this.category = this.expense.category?.system_category?.toLowerCase();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
this.isMileageExpense = this.expense.category?.name?.toLowerCase() === 'mileage';
this.isPerDiem = this.expense.category?.name?.toLowerCase() === 'per diem';
this.category = this.expense.tx_org_category?.toLowerCase();
this.expense.isDraft = this.transactionService.getIsDraft(this.expense);
this.expense.isPolicyViolated = this.expense.tx_policy_flag;
this.expense.isCriticalPolicyViolated = this.transactionService.getIsCriticalPolicyViolated(this.expense);
this.expense.vendorDetails = this.transactionService.getVendorDetails(this.expense);
this.category = this.expense.category?.name?.toLowerCase();
this.isDraft = this.transactionService.getIsDraft(this.expense);
this.isMileageExpense = this.expense.category?.system_category === 'Mileage';
this.isPerDiem = this.expense.category?.system_category === 'Per Diem';
this.category = this.expense.category?.system_category?.toLowerCase();
this.isDraft = this.transactionService.getIsDraft(this.expense);
🤖 Prompt for AI Agents
In src/app/shared/components/expenses-card/expenses-card.component.ts around
lines 273 to 277, the code currently detects mileage/per diem by inspecting
expense.category.name (user-editable); change it to use the canonical
expense.category.system_category instead: set this.isMileageExpense =
this.expense.category?.system_category?.toLowerCase() === 'mileage' and
this.isPerDiem = this.expense.category?.system_category?.toLowerCase() === 'per
diem', and set this.category =
this.expense.category?.system_category?.toLowerCase() (falling back to undefined
if absent) while leaving the isDraft assignment unchanged.

Comment on lines 348 to 354
canAddAttachment(): boolean {
return (
!this.isFromViewReports &&
!(this.isMileageExpense || this.isPerDiem || this.expense.tx_file_ids || this.isFromPotentialDuplicates) &&
!(this.isMileageExpense || this.isPerDiem || this.expense.file_ids || this.isFromPotentialDuplicates) &&
!this.isSelectionModeEnabled
);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Attachment add check should test length; array truthiness bites. Padam super after this.

-      !(this.isMileageExpense || this.isPerDiem || this.expense.file_ids || this.isFromPotentialDuplicates) &&
+      !(this.isMileageExpense ||
+        this.isPerDiem ||
+        (this.expense.file_ids?.length ?? 0) > 0 ||
+        this.isFromPotentialDuplicates) &&
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
canAddAttachment(): boolean {
return (
!this.isFromViewReports &&
!(this.isMileageExpense || this.isPerDiem || this.expense.tx_file_ids || this.isFromPotentialDuplicates) &&
!(this.isMileageExpense || this.isPerDiem || this.expense.file_ids || this.isFromPotentialDuplicates) &&
!this.isSelectionModeEnabled
);
}
canAddAttachment(): boolean {
return (
!this.isFromViewReports &&
!(this.isMileageExpense ||
this.isPerDiem ||
(this.expense.file_ids?.length ?? 0) > 0 ||
this.isFromPotentialDuplicates) &&
!this.isSelectionModeEnabled
);
}
🤖 Prompt for AI Agents
In src/app/shared/components/expenses-card/expenses-card.component.ts around
lines 348 to 354, the canAddAttachment() check treats expense.file_ids as a
boolean which is incorrect for arrays; update the condition to test the array
length (e.g. use !this.expense.file_ids?.length or
!(Array.isArray(this.expense.file_ids) && this.expense.file_ids.length > 0)) so
existing logic still prevents adding attachments when file_ids is a non-empty
array, while preserving the other flags.

Comment on lines 442 to 446
matchReceiptWithEtxn(fileObj: FileObject): void {
this.expense.tx_file_ids = [];
this.expense.tx_file_ids.push(fileObj.id);
fileObj.transaction_id = this.expense.tx_id;
this.expense.file_ids = [];
this.expense.file_ids.push(fileObj.id);
fileObj.transaction_id = this.expense.id;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Don’t nuke existing file_ids — push safely. Semma careful.

-    this.expense.file_ids = [];
-    this.expense.file_ids.push(fileObj.id);
+    this.expense.file_ids = [...(this.expense.file_ids ?? []), fileObj.id];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
matchReceiptWithEtxn(fileObj: FileObject): void {
this.expense.tx_file_ids = [];
this.expense.tx_file_ids.push(fileObj.id);
fileObj.transaction_id = this.expense.tx_id;
this.expense.file_ids = [];
this.expense.file_ids.push(fileObj.id);
fileObj.transaction_id = this.expense.id;
}
matchReceiptWithEtxn(fileObj: FileObject): void {
- this.expense.file_ids = [];
this.expense.file_ids = [...(this.expense.file_ids ?? []), fileObj.id];
fileObj.transaction_id = this.expense.id;
}
🤖 Prompt for AI Agents
In src/app/shared/components/expenses-card/expenses-card.component.ts around
lines 442 to 446, the current code clears expense.file_ids before adding the new
file and thereby nukes existing attachments; change it to safely append the new
file id: ensure expense and expense.file_ids exist (initialize to an array if
missing), push the new fileObj.id only if it’s not already present to avoid
duplicates, and then set fileObj.transaction_id = this.expense.id; also handle
the case where this.expense may be null/undefined by returning early or throwing
as appropriate.

@github-actions
Copy link

github-actions bot commented Sep 9, 2025

PR description must contain a link to a ClickUp (case-insensitive)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (11)
src/app/core/services/transactions-outbox.service.ts (2)

192-196: Guard against negative index; avoid deleting the last entry by mistake.

If index is -1, splice(-1, 1) nukes the last item. Not today, boss.

-  deleteOfflineExpense(index: number): null {
-    this.queue.splice(index, 1);
-    this.saveQueue();
-    return null;
-  }
+  deleteOfflineExpense(index: number): null {
+    if (index < 0 || index >= this.queue.length) {
+      return null;
+    }
+    this.queue.splice(index, 1);
+    this.saveQueue();
+    return null;
+  }

198-205: indexOf on objects will miss; compute indexes by id/local_id and ignore -1.

Using indexOf on cloned objects returns -1; sorting and deleting -1 will pop the last entry. Kaboom. Match by id or local_id; filter out misses.

-  deleteBulkOfflineExpenses(pendingTransactions: Partial<Transaction>[], deleteExpenses: Partial<Transaction>[]): void {
-    const indexes = deleteExpenses.map((offlineExpense) => indexOf(pendingTransactions, offlineExpense));
-    // We need to delete last element of this list first
-    indexes.sort((a, b) => b - a);
-    indexes.forEach((index) => {
-      this.deleteOfflineExpense(index);
-    });
-  }
+  deleteBulkOfflineExpenses(pending: Partial<Transaction>[], toDelete: Partial<Transaction>[]): void {
+    const keyOf = (t: Partial<Transaction>) => (t.id as string) ?? ((t as any).local_id as string) ?? null;
+    const indexes = toDelete
+      .map((item) => {
+        const k = keyOf(item);
+        if (k) {
+          return pending.findIndex((p) => keyOf(p) === k);
+        }
+        // Best-effort fallback for truly keyless items
+        return pending.findIndex(
+          (p) =>
+            p.amount === item.amount &&
+            p.currency === item.currency &&
+            (p.spent_at as any)?.toString?.() === (item.spent_at as any)?.toString?.()
+        );
+      })
+      .filter((i) => i >= 0)
+      .sort((a, b) => b - a);
+    indexes.forEach((i) => this.deleteOfflineExpense(i));
+  }

Also remove the now-unused lodash indexOf import.

src/app/core/services/transactions-outbox.service.spec.ts (1)

233-240: Fix brittle expectation: don’t expect deletion with -1.

With robust key-based matching, -1 should never be used. Update the test to derive deleteExpenses from getPendingExpenses() and assert only valid indexes are deleted.

-  it('deleteBulkOfflineExpenses(): should delete bulk offline expenses', () => {
-    spyOn(transactionsOutboxService, 'deleteOfflineExpense').and.returnValues(null);
-    const pendingTransactions = [expenseList2[0]];
-    const deleteExpenses = expenseList2;
-    transactionsOutboxService.deleteBulkOfflineExpenses(pendingTransactions, deleteExpenses);
-    expect(transactionsOutboxService.deleteOfflineExpense).toHaveBeenCalledTimes(2);
-    expect(transactionsOutboxService.deleteOfflineExpense).toHaveBeenCalledWith(0);
-    expect(transactionsOutboxService.deleteOfflineExpense).toHaveBeenCalledWith(-1);
-  });
+  it('deleteBulkOfflineExpenses(): should delete only matching offline expenses by id/local_id', () => {
+    spyOn(transactionsOutboxService, 'deleteOfflineExpense').and.returnValues(null);
+    // Arrange pending based on the service’s shape
+    transactionsOutboxService.queue = cloneDeep(outboxQueueData1);
+    const pending = transactionsOutboxService.getPendingExpenses();
+    const deleteExpenses = [pending[0]]; // one valid match
+
+    // Act
+    transactionsOutboxService.deleteBulkOfflineExpenses(pending, deleteExpenses);
+
+    // Assert
+    expect(transactionsOutboxService.deleteOfflineExpense).toHaveBeenCalledTimes(1);
+    expect(transactionsOutboxService.deleteOfflineExpense).toHaveBeenCalledWith(0);
+  });
src/app/fyle/my-expenses/my-expenses.page.html (1)

222-235: Use Transaction fields (spent_at/created_at), not legacy tx_ fields.

Thalaivar tip: pendingExpenses are Transactions; tx_txn_dt/tx_created_at will be undefined.

-        @for (pendingExpense of pendingExpenses; track pendingExpense; let i = $index) {
+        @for (pendingExpense of pendingExpenses; track pendingExpense; let i = $index) {
           <app-expense-card
             [expense]="pendingExpense"
             [isFirstOfflineExpense]="i === 0"
-            [previousExpenseTxnDate]="pendingExpenses[i - 1]?.tx_txn_dt"
-            [previousExpenseCreatedAt]="pendingExpenses[i - 1]?.tx_created_at"
+            [previousExpenseTxnDate]="pendingExpenses[i - 1]?.spent_at"
+            [previousExpenseCreatedAt]="pendingExpenses[i - 1]?.created_at"
             [isSelectionModeEnabled]="selectionMode"
             [selectedElements]="selectedOutboxExpenses"
             [isOutboxExpense]="true"
             (setMultiselectMode)="switchOutboxSelectionMode($event)"
             (cardClickedForSelection)="selectOutboxExpense($event)"
           >
           </app-expense-card>
         }
src/app/fyle/my-expenses/my-expenses.page.ts (3)

903-921: Simplify sync flow; calling sync once is enough.

from(this.pendingExpenses).pipe(switchMap(sync)) cancels repeatedly and is confusing. Call sync() directly; then refresh if queue is empty.

-  syncOutboxExpenses(): void {
-    this.pendingExpenses = this.transactionOutboxService.getPendingExpenses();
-    if (this.pendingExpenses.length > 0) {
-      this.syncing = true;
-      from(this.pendingExpenses)
-        .pipe(
-          switchMap(() => from(this.transactionOutboxService.sync())),
-          finalize(() => {
-            this.syncing = false;
-            const pendingExpenses = this.transactionOutboxService.getPendingExpenses();
-            if (pendingExpenses.length === 0) {
-              this.doRefresh();
-              this.pendingExpenses = [];
-            }
-          }),
-        )
-        .subscribe(noop);
-    }
-  }
+  syncOutboxExpenses(): void {
+    this.pendingExpenses = this.transactionOutboxService.getPendingExpenses();
+    if (this.pendingExpenses.length === 0) return;
+    this.syncing = true;
+    from(this.transactionOutboxService.sync())
+      .pipe(
+        finalize(() => {
+          this.syncing = false;
+          this.pendingExpenses = this.transactionOutboxService.getPendingExpenses();
+          if (this.pendingExpenses.length === 0) this.doRefresh();
+        }),
+      )
+      .subscribe(noop);
+  }

1105-1139: Selection by deep-equal is brittle; compare by id/local_id.

Offline entries often lack id; add and use local_id from the outbox for stable comparisons. Faster, safer, stylish.

-  selectOutboxExpense(expense: Partial<Transaction>): void {
-    let isSelectedElementsIncludesExpense = false;
-    if (expense.id) {
-      isSelectedElementsIncludesExpense = this.selectedOutboxExpenses.some((exp) => expense.id === exp.id);
-    } else {
-      isSelectedElementsIncludesExpense = this.selectedOutboxExpenses.some((exp) => isEqual(exp, expense));
-    }
+  selectOutboxExpense(expense: Partial<Transaction>): void {
+    const keyOf = (e: Partial<Transaction>) => (e.id as string) ?? ((e as any).local_id as string) ?? null;
+    const key = keyOf(expense);
+    const includes = key
+      ? this.selectedOutboxExpenses.some((e) => keyOf(e) === key)
+      : this.selectedOutboxExpenses.some((e) => isEqual(e, expense));
 
-    if (isSelectedElementsIncludesExpense) {
-      if (expense.id) {
-        this.selectedOutboxExpenses = this.selectedOutboxExpenses.filter((exp) => exp.id !== expense.id);
-      } else {
-        this.selectedOutboxExpenses = this.selectedOutboxExpenses.filter((exp) => !isEqual(exp, expense));
-      }
+    if (includes) {
+      this.selectedOutboxExpenses = key
+        ? this.selectedOutboxExpenses.filter((e) => keyOf(e) !== key)
+        : this.selectedOutboxExpenses.filter((e) => !isEqual(e, expense));
     } else {
       this.selectedOutboxExpenses.push(expense);
     }

1744-1753: onSelectAll pending path is correct. Consider copying, not aliasing.

Assigning array by reference couples UI to service state. Clone for safety.

-        this.selectedOutboxExpenses = this.pendingExpenses;
+        this.selectedOutboxExpenses = cloneDeep(this.pendingExpenses);
src/app/fyle/my-expenses/my-expenses.page.spec.ts (4)

1279-1291: Drop the formatTransactions unit test.

Component no longer exposes formatTransactions. Remove this block to appease the test gods.

-  it('formatTransactions(): should format transactions correctly', () => {
-    const unformattedTransactions = unformattedTxnData;
-    const formattedTransactions = component.formatTransactions(unformattedTransactions);
-
-    expect(formattedTransactions.length).toBe(unformattedTransactions.length);
-    expect(formattedTransactions).toEqual(expectedFormattedTransaction);
-  });

2956-2965: Fix property rename in tests: pendingTransactions → pendingExpenses.

This is the pipeline error. Change the assignment and expectations accordingly.

-      component.pendingTransactions = [];
+      component.pendingExpenses = [];

3171-3181: Select-all path: adjust to pendingExpenses and outbox list.

Match the component’s new names and behavior.

-    it('should select all pending transactions and update stats', () => {
-      component.pendingTransactions = expenseList4;
+    it('should select all pending expenses and update stats', () => {
+      component.pendingExpenses = expenseList4 as any;
       transactionService.getReportableExpenses.and.returnValue(expenseList4);
       spyOn(component, 'setOutboxExpenseStatsOnSelect');
 
       component.onSelectAll(true);
 
-      expect(transactionService.getReportableExpenses).toHaveBeenCalledOnceWith(expenseList4);
+      expect(transactionService.getReportableExpenses).toHaveBeenCalledOnceWith(expenseList4 as any);
       expect(component.setOutboxExpenseStatsOnSelect).toHaveBeenCalledTimes(1);
       expect(component.isReportableExpensesSelected).toBeTrue();
     });

523-531: Remove orphaned spy on formatTransactions in ionViewWillEnter() suite.

This spy breaks compilation since the method is gone.

-      spyOn(component, 'formatTransactions').and.returnValue(apiExpenseRes);
♻️ Duplicate comments (1)
src/app/fyle/my-expenses/my-expenses.page.ts (1)

1099-1102: Null-safe sum for optional amount. Superstar move.

amount is optional; guard with ?? 0 to avoid NaN.

-      amount: this.selectedOutboxExpenses.reduce((acc, exp) => acc + exp.amount, 0),
+      amount: this.selectedOutboxExpenses.reduce((acc, exp) => acc + (exp.amount ?? 0), 0),
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 536b1d3 and 2b5f196.

📒 Files selected for processing (5)
  • src/app/core/services/transactions-outbox.service.spec.ts (1 hunks)
  • src/app/core/services/transactions-outbox.service.ts (2 hunks)
  • src/app/fyle/my-expenses/my-expenses.page.html (5 hunks)
  • src/app/fyle/my-expenses/my-expenses.page.spec.ts (2 hunks)
  • src/app/fyle/my-expenses/my-expenses.page.ts (8 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{service,pipe,directive}.ts

📄 CodeRabbit inference engine (.cursor/rules/atomics-i18n-key-pattern.mdc)

**/*.{service,pipe,directive}.ts: i18n keys for services, pipes, and directives must use the schema: services.., pipes.., directives..
Convert kebab-case file names to camelCase and remove the suffix (Service, Pipe, Directive) when generating i18n keys for services, pipes, and directives
Use only the three roots: services, pipes, directives for i18n key grouping
Use meaningful, semantic, and context-aware key names for i18n keys; avoid generic names like message1, text1, error1

Files:

  • src/app/core/services/transactions-outbox.service.ts
🧠 Learnings (4)
📚 Learning: 2024-11-18T06:55:17.972Z
Learnt from: sumrender
PR: fylein/fyle-mobile-app#3265
File: src/app/fyle/personal-cards-matched-expenses/expense-preview/expense-preview.component.spec.ts:35-37
Timestamp: 2024-11-18T06:55:17.972Z
Learning: In `src/app/fyle/add-edit-expense/add-edit-expense.page.ts`, the `unmatchExpense` method is used for corporate card expenses, not for personal card expenses.

Applied to files:

  • src/app/fyle/my-expenses/my-expenses.page.html
  • src/app/fyle/my-expenses/my-expenses.page.spec.ts
  • src/app/fyle/my-expenses/my-expenses.page.ts
📚 Learning: 2024-11-04T13:10:36.868Z
Learnt from: arjunaj5
PR: fylein/fyle-mobile-app#3242
File: src/app/fyle/my-expenses/my-expenses.page.spec.ts:967-968
Timestamp: 2024-11-04T13:10:36.868Z
Learning: In `src/app/fyle/my-expenses/my-expenses.page.spec.ts`, when testing private methods like `filterDEIncompleteExpenses`, it's acceptable to use `//ts-ignore` to spy on them in unit tests.

Applied to files:

  • src/app/fyle/my-expenses/my-expenses.page.spec.ts
📚 Learning: 2024-10-23T11:45:16.974Z
Learnt from: bistaastha
PR: fylein/fyle-mobile-app#3240
File: src/app/fyle/manage-corporate-cards/manage-corporate-cards.page.spec.ts:293-354
Timestamp: 2024-10-23T11:45:16.974Z
Learning: In 'src/app/fyle/manage-corporate-cards/manage-corporate-cards.page.spec.ts', the flags 'isVisaRTFEnabled$', 'isMastercardRTFEnabled$', and 'isYodleeEnabled$' do not have rapidly changing values, so tests for rapid changes in these flags are unnecessary.

Applied to files:

  • src/app/fyle/my-expenses/my-expenses.page.spec.ts
📚 Learning: 2024-10-23T08:40:59.542Z
Learnt from: bistaastha
PR: fylein/fyle-mobile-app#3240
File: src/app/fyle/manage-corporate-cards/manage-corporate-cards.page.spec.ts:200-200
Timestamp: 2024-10-23T08:40:59.542Z
Learning: In 'src/app/fyle/manage-corporate-cards/manage-corporate-cards.page.spec.ts', moving 'component.isAddCorporateCardVisible$ = of(true);' to the top-level beforeEach block causes tests to fail, so mock values need to be set in individual tests.

Applied to files:

  • src/app/fyle/my-expenses/my-expenses.page.spec.ts
🧬 Code graph analysis (3)
src/app/core/services/transactions-outbox.service.spec.ts (1)
src/app/core/mock-data/outbox-queue.data.ts (1)
  • outboxQueueData1 (6-22)
src/app/core/services/transactions-outbox.service.ts (2)
src/app/core/models/v1/transaction.model.ts (1)
  • Transaction (8-142)
src/app/core/services/platform/v1/spender/expenses.service.ts (1)
  • deleteExpenses (392-400)
src/app/fyle/my-expenses/my-expenses.page.ts (1)
src/app/core/models/v1/transaction.model.ts (1)
  • Transaction (8-142)
🪛 GitHub Actions: Unit Tests
src/app/fyle/my-expenses/my-expenses.page.spec.ts

[error] 527-527: Argument of type '"formatTransactions"' is not assignable to parameter of type 'keyof MyExpensesPage'.


[error] 2956-2956: Property 'pendingTransactions' does not exist on type 'MyExpensesPage'.

🔇 Additional comments (5)
src/app/fyle/my-expenses/my-expenses.page.ts (2)

134-142: Rename to pendingExpenses and selectedOutboxExpenses looks good.

Clean, consistent with the service refactor.


1640-1652: Return type is Observable, but the offline branch is side-effect only.

LGTM for now since caller awaits completion via popover flow. Just ensure callers subscribe.

Would you like me to scan call sites to ensure subscribe() is present where needed?

src/app/fyle/my-expenses/my-expenses.page.spec.ts (2)

210-214: Spy should match service rename. Good.

getPendingExpenses is the right method post-refactor.


232-240: Ignore outdated bulk-delete assertion comment
My friend, the tests never inspect index values or “–1” deletes—there’s no such logic here. The suggestion about ignoring non-matching items in index assertions does not apply.

Likely an incorrect or invalid review comment.

src/app/core/services/transactions-outbox.service.spec.ts (1)

213-221: New test name matches method rename. Solid.

Proceed.

Comment on lines +188 to 190
getPendingExpenses(): Partial<Transaction>[] {
return this.queue.map((entry) => ({ ...entry.transaction, dataUrls: entry.dataUrls }));
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Rename looks fine, but add a stable local_id for offline items.

Super move da! getPendingExpenses() aligns the API with Transactions. To make downstream selection/deletion rock-solid, include a stable local_id on each outbox transaction (set in addEntry) so callers can reliably identify items even without id.

Apply this in addEntry:

   addEntry(
     transaction: Partial<Transaction>,
     dataUrls: { url: string; type: string }[],
     comments?: string[],
   ): Promise<void> {
-    this.queue.push({
-      transaction,
+    const localId = (crypto as any)?.randomUUID?.() ?? Date.now().toString();
+    this.queue.push({
+      transaction: { ...(transaction as any), local_id: localId } as any,
       dataUrls,
       comments,
     });
     return this.saveQueue();
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
getPendingExpenses(): Partial<Transaction>[] {
return this.queue.map((entry) => ({ ...entry.transaction, dataUrls: entry.dataUrls }));
}
addEntry(
transaction: Partial<Transaction>,
dataUrls: { url: string; type: string }[],
comments?: string[],
): Promise<void> {
const localId = (crypto as any)?.randomUUID?.() ?? Date.now().toString();
this.queue.push({
transaction: { ...(transaction as any), local_id: localId } as any,
dataUrls,
comments,
});
return this.saveQueue();
}
🤖 Prompt for AI Agents
In src/app/core/services/transactions-outbox.service.ts around lines 188-190,
getPendingExpenses currently maps entries to transactions but doesn't include a
stable local_id for offline items; modify addEntry to generate and store a
stable local_id (e.g. a UUID or monotonic counter) on the outbox entry when
created and ensure that getPendingExpenses returns that local_id on each
returned transaction object (include it in the mapped object alongside
transaction and dataUrls) so downstream selection/deletion can reliably identify
items without a server id.

Comment on lines +1759 to 1769
transactionOutboxService.getPendingExpenses.and.returnValues(mockPendingTransactions, []);
transactionOutboxService.sync.and.resolveTo(undefined);
spyOn(component, 'doRefresh');

component.syncOutboxExpenses();
tick(100);

expect(component.pendingTransactions).toEqual([]);
expect(component.pendingExpenses).toEqual([]);
expect(component.formatTransactions).toHaveBeenCalledTimes(2);
expect(transactionOutboxService.getPendingTransactions).toHaveBeenCalledTimes(2);
expect(transactionOutboxService.getPendingExpenses).toHaveBeenCalledTimes(2);
expect(transactionOutboxService.sync).toHaveBeenCalledTimes(1);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Remove formatTransactions spying and assertions; method no longer exists.

Why fear when Rajini is here? Trim the test to assert syncing + pendingExpenses + service calls only.

-    spyOn(component, 'formatTransactions').and.returnValues(mockFormattedTransactions, []);
     transactionOutboxService.getPendingExpenses.and.returnValues(mockPendingTransactions, []);
     transactionOutboxService.sync.and.resolveTo(undefined);
     spyOn(component, 'doRefresh');
 
     component.syncOutboxExpenses();
     tick(100);
 
-    expect(component.pendingExpenses).toEqual([]);
-    expect(component.formatTransactions).toHaveBeenCalledTimes(2);
+    expect(component.pendingExpenses).toEqual([]);
     expect(transactionOutboxService.getPendingExpenses).toHaveBeenCalledTimes(2);
     expect(transactionOutboxService.sync).toHaveBeenCalledTimes(1);
     expect(component.syncing).toBeFalse();
     expect(component.doRefresh).toHaveBeenCalledTimes(1);
🤖 Prompt for AI Agents
In src/app/fyle/my-expenses/my-expenses.page.spec.ts around lines 1759 to 1769,
remove any spying and assertions related to the now-removed formatTransactions
method and simplify the test to only assert that sync was invoked,
pendingExpenses ends up empty, and the transactionOutboxService methods were
called the expected number of times; specifically delete spyOn/expect lines for
formatTransactions, keep the getPendingExpenses and sync stubs, preserve the
pendingExpenses assertion and the service call count assertions, and ensure
tick(100) and any refresh spy are adjusted/removed only if they are tied to
formatTransactions so the test only verifies syncing, pendingExpenses state, and
service interactions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/XL Extra Large PR

Development

Successfully merging this pull request may close these issues.

2 participants