Skip to content

Conversation

@Hrishabh17
Copy link
Member

@Hrishabh17 Hrishabh17 commented Jan 27, 2026

Summary by CodeRabbit

  • New Features

    • System comments now record export events (stuck-export retriggers, export-error summaries, payment-skip reasons) and warn when billable line items lack required dimensions.
  • Bug Fixes

    • Consolidated and made reliable the creation of system comments across export flows, including error and exception paths.
  • Tests

    • Significantly expanded tests covering system comment generation across export modules and failure scenarios.
  • Chores

    • Added a database migration and updated supporting repository pointer.

✏️ Tip: You can customize this high-level summary in your review settings.

@github-actions github-actions bot added the size/XL Extra Large PR label Jan 27, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 27, 2026

Walkthrough

Centralizes system comment collection across Sage Intacct export flows, adds billable line-item validation that emits system comments when required dimensions are missing, introduces new system-comment enum members, adds tests and fixtures for these behaviors, and adds a DB migration to run raw SQL.

Changes

Cohort / File(s) Summary
System Comment Enums
apps/workspaces/enums.py
Added new SystemCommentSourceEnum members (CREATE_BILL_LINEITEMS, CREATE_JOURNAL_ENTRY, CREATE_EXPENSE_REPORT, RETRIGGER_STUCK_EXPORTS), new SystemCommentIntentEnum members (BILLABLE_DISABLED, EXPORT_SUMMARY_NOT_UPDATED, EXPORT_RETRIGGERED, EXPORT_MODULE_RETIRED), and new SystemCommentReasonEnum entries for billable validation, error-state handling, retriggers, and payment-skip reasons.
Export Pipeline & Retrigger Flow
apps/sage_intacct/tasks.py, apps/internal/tasks.py
Introduced system_comments accumulation across export flows (Bills, Expense Reports, Journal Entries, Charge Card Transactions, AP payments, reimbursements, retrigger). Replaced per-block creation with SystemComment.bulk_create_comments(system_comments) after processing; ensured exception-path persistence and added handling for auto-export previous ERROR state. Updated validate_for_skipping_payment(..., system_comments: list = None) signature.
Sage Intacct — Billable Lineitem Validation
apps/sage_intacct/models.py
Added pre-create checks in multiple lineitem creation functions to detect missing customer_id or item_id when billable is true. When missing, append source-specific system comment entries (source, intent, reason, info) before continuing to update/create the lineitem.
Tests & Fixtures
tests/conftest.py, tests/test_system_comments/test_system_comments.py
Added get_or_create_task_log fixture (callable) and extensive tests covering billable-dimension warnings, export-error handling, stuck-export retriggering, payment-skipping logic, and many error paths with mocks and assertions. Note: fixture duplication present in conftest.py. Large test additions (~+1218 lines).
Migration & DB Migrations Submodule
apps/internal/migrations/0029_auto_generated_sql.py, fyle-integrations-db-migrations
Added migration that runs raw SQL (delete_workspace.sql) via safe_run_sql; submodule pointer updated to new commit.

Sequence Diagram(s)

sequenceDiagram
    participant Export as Export Function
    participant Validator as Billable Validator
    participant SystemSvc as SystemComment Service
    participant External as Intacct API
    participant DB as Database

    Export->>SystemSvc: init system_comments = []
    loop each expense_group / lineitem
        Export->>Validator: check billable dimensions
        alt missing customer_id/item_id
            Validator->>SystemSvc: append comment(source,intent,reason,info)
        end
        Export->>External: prepare/send payload
        alt API error or export-state condition
            External-->>Export: error/state
            Export->>SystemSvc: append error/skip/retrigger comment
        end
    end
    alt system_comments not empty
        SystemSvc->>DB: bulk_create_comments(system_comments)
        DB-->>SystemSvc: persisted
    end
    Export->>Export: finish flow
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 I hopped through flows where comments bloom,
I flagged the missing bits and chased the gloom,
I gathered whispers, batched them neat in rows,
Then sent them off so everyone now knows. 🥕

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Description check ⚠️ Warning No pull request description was provided by the author, leaving the change intent, rationale, and scope completely undocumented. Add a comprehensive PR description following the repository template, including what changes were made, why they were made, and any relevant context or testing information.
Title check ❓ Inconclusive The title 'feat: system comments for p1 areas' is vague and generic, using the non-descriptive term 'p1 areas' without clarifying which specific functionality or areas are affected. Revise the title to be more specific about the primary change, such as 'feat: add system comments for stuck exports and billable validation' or similar descriptive language.
✅ Passed checks (1 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 98.04% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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
Copy link

Coverage

Coverage Report
FileStmtsMissCoverMissing
apps
   exceptions.py471177%27, 33, 39–44, 50, 56–61, 67, 73, 79–80
apps/fyle
   actions.py124794%214, 263, 319–323
   constants.py10100% 
   helpers.py2351494%50–57, 66, 376, 470, 477–480, 483–484
   models.py3382194%315–317, 321–323, 327–329, 333–335, 339–341, 345–347, 408, 530–551, 574, 591
   queue.py500100% 
   serializers.py410100% 
   signals.py35683%22, 55–56, 69–71
   tasks.py5468784%132, 143–144, 147–148, 151–152, 174–175, 231–251, 263–268, 311–314, 339–343, 353–361, 400–410, 525–585, 906–910, 1048, 1059–1060, 1085–1086
   views.py1721591%84–91, 145, 245–246, 290–291, 316–320, 413–417
apps/internal
   actions.py39295%26–27
   helpers.py361364%18–35
   serializers.py300100% 
   tasks.py906528%31–35, 55–112, 121–172
   views.py83199%39
apps/internal/services
   e2e_setup.py590100% 
   fixture_factory.py59395%218–220
apps/mappings
   constants.py20100% 
   exceptions.py71692%69–71, 79–81
   helpers.py300100% 
   models.py500100% 
   schedules.py150100% 
   serializers.py100100% 
   signals.py1111487%90–92, 127, 179–184, 190–194, 200–203, 209–210
   tasks.py1732983%117–118, 141–142, 145, 209–226, 254, 257, 260, 266, 269, 275, 314–316, 327, 399
   utils.py340100% 
   views.py46491%34–35, 114–115
apps/sage_intacct
   actions.py29486%21–22, 49–50
   connector.py8052597%133, 309, 321, 327, 403, 1414, 1713, 1840–1841, 1892–1893, 1914–1915, 1944–1945, 1953–1964, 1996–1997, 2017–2018
   dependent_fields.py2904286%345, 381–384, 413–422, 424–425, 427–429, 431–432, 434–435, 437–438, 442–444, 488, 508, 548–550, 596–598, 646–652, 656–657
   enums.py250100% 
   helpers.py56689%38–41, 58, 95
   models.py9886793%78–79, 261–271, 399, 406, 450, 471–488, 560, 572, 622, 628, 663, 713–715, 729–742, 753, 789, 841–842, 940, 981, 1209, 1304–1307, 1351, 1353, 1444, 1548–1589, 1687, 1713, 1824, 2198–2200, 2262
   queue.py1711691%45, 53, 58, 105, 130–133, 173, 187, 205–208, 243, 267–270, 305, 330–333
   serializers.py350100% 
   tasks.py158719388%105, 119, 152, 188–189, 193–196, 198, 201, 251, 286–287, 407–408, 414, 417, 460–461, 502–507, 572, 578, 586, 750, 765–766, 786–789, 865–866, 870, 889, 904, 909, 914, 921, 927, 930–932, 935–938, 941–943, 963, 1016–1017, 1103–1104, 1108, 1132, 1186, 1199–1207, 1298–1299, 1380–1381, 1383, 1387–1390, 1409, 1455–1458, 1463, 1476–1484, 1575–1576, 1669–1670, 1673, 1699, 1719, 1730, 1741, 1753, 1764, 1767–1774, 1777–1785, 1788–1795, 1819, 1878–1881, 1964, 1976–1985, 2027, 2030–2035, 2038–2042, 2211–2215, 2314–2315, 2521–2531, 2534, 2543–2548, 2582–2584, 2618–2621, 2689–2693, 2702, 2791–2796, 2809, 2824, 2839
   utils.py8903396%718–722, 867, 1186, 1202–1208, 1217–1218, 1944–1948, 1976–1980, 2008–2012, 2056–2060, 2114–2118, 2139, 2148, 2157, 2166–2167
   views.py1453377%127–129, 134–135, 141–142, 185–188, 258–288, 299–314, 327–331
apps/sage_intacct/errors
   errors.py20100% 
   helpers.py47198%80
apps/sage_intacct/exports
   ap_payments.py130100% 
   bills.py280100% 
   charge_card_transactions.py220100% 
   expense_reports.py280100% 
   helpers.py56296%68, 141
   journal_entries.py76889%177–180, 316–319
   reimbursements.py130100% 
apps/tasks
   models.py690100% 
   serializers.py60100% 
   views.py370100% 
apps/users
   helpers.py120100% 
   models.py530100% 
   views.py170100% 
apps/workspaces
   actions.py46980%65, 84–85, 103, 122–133, 147
   enums.py840100% 
   helpers.py80100% 
   models.py1760100% 
   permissions.py34974%33, 59–67
   serializers.py43198%54
   signals.py310100% 
   system_comments.py60100% 
   tasks.py175995%53–59, 187–194, 235, 267–268, 379
   utils.py90100% 
   views.py320997%107–109, 136–139, 665–666
apps/workspaces/apis/advanced_settings
   serializers.py80396%260, 263, 266
   triggers.py100100% 
   views.py110100% 
apps/workspaces/apis/errors
   serializers.py200100% 
   views.py150100% 
apps/workspaces/apis/export_settings
   helpers.py730100% 
   serializers.py103397%267, 270, 273
   triggers.py39295%32–33
   views.py110100% 
apps/workspaces/apis/import_settings
   serializers.py1411887%232–238, 243–249, 257–265, 283, 286, 314, 317–318, 326
   triggers.py47198%31
   views.py330100% 
fyle_integrations_imports
   dataclasses.py310100% 
   models.py230100% 
   queues.py34585%24, 99, 136, 148–149
   signals.py100100% 
   tasks.py1278136%69–111, 120–126, 139–208, 212–227, 252–255, 257, 259, 264
fyle_integrations_imports/modules
   base.py1881095%75, 91–92, 95, 126, 230–231, 237–238, 351
   categories.py1773381%80, 96, 99–100, 103–104, 272–273, 283, 296, 298, 342, 351–355, 364–410
   cost_centers.py961288%132–133, 152–153, 162, 178, 180, 226, 235–239
   expense_custom_fields.py121794%83–88, 247, 263, 265, 294
   merchants.py1062576%80–83, 111–129, 155–162, 179–180, 190, 203, 205, 235, 238–242
   projects.py1234067%88, 103–168, 188–189, 199, 223, 270, 284–288
   tax_groups.py180100% 
   webhook_attributes.py130695%98, 100–102, 213–214
workers
   actions.py210100% 
   helpers.py420100% 
   worker.py56591%44–45, 77–78, 125
TOTAL10845101691% 

Tests Skipped Failures Errors Time
776 0 💤 0 ❌ 0 🔥 1m 6s ⏱️

@github-actions
Copy link

Failure. Coverage is below 90%.

Diff Coverage
Diff: origin/master..HEAD, staged and unstaged changes

apps/internal/tasks.py (36.4%): Missing lines 92-96,111-112
apps/sage_intacct/models.py (100%)
apps/sage_intacct/tasks.py (81.6%): Missing lines 750,1199-1203,1387-1390,1455-1458,1476-1480,1767-1771,1777-1781,1788-1792,1879-1881
apps/workspaces/enums.py (100%)

Total: 253 lines
Missing: 44 lines
Coverage: 82%

@github-actions
Copy link

Coverage

Coverage Report
FileStmtsMissCoverMissing
apps
   exceptions.py471177%27, 33, 39–44, 50, 56–61, 67, 73, 79–80
apps/fyle
   actions.py124794%214, 263, 319–323
   constants.py10100% 
   helpers.py2351494%50–57, 66, 376, 470, 477–480, 483–484
   models.py3382194%315–317, 321–323, 327–329, 333–335, 339–341, 345–347, 408, 530–551, 574, 591
   queue.py500100% 
   serializers.py410100% 
   signals.py35683%22, 55–56, 69–71
   tasks.py5468784%132, 143–144, 147–148, 151–152, 174–175, 231–251, 263–268, 311–314, 339–343, 353–361, 400–410, 525–585, 906–910, 1048, 1059–1060, 1085–1086
   views.py1721591%84–91, 145, 245–246, 290–291, 316–320, 413–417
apps/internal
   actions.py39295%26–27
   helpers.py361364%18–35
   serializers.py300100% 
   tasks.py903264%31–35, 61–66, 109, 121–172
   views.py83199%39
apps/internal/services
   e2e_setup.py590100% 
   fixture_factory.py59395%218–220
apps/mappings
   constants.py20100% 
   exceptions.py71692%69–71, 79–81
   helpers.py300100% 
   models.py500100% 
   schedules.py150100% 
   serializers.py100100% 
   signals.py1111487%90–92, 127, 179–184, 190–194, 200–203, 209–210
   tasks.py1732983%117–118, 141–142, 145, 209–226, 254, 257, 260, 266, 269, 275, 314–316, 327, 399
   utils.py340100% 
   views.py46491%34–35, 114–115
apps/sage_intacct
   actions.py29486%21–22, 49–50
   connector.py8052597%133, 309, 321, 327, 403, 1414, 1713, 1840–1841, 1892–1893, 1914–1915, 1944–1945, 1953–1964, 1996–1997, 2017–2018
   dependent_fields.py2904286%345, 381–384, 413–422, 424–425, 427–429, 431–432, 434–435, 437–438, 442–444, 488, 508, 548–550, 596–598, 646–652, 656–657
   enums.py250100% 
   helpers.py56689%38–41, 58, 95
   models.py9886793%78–79, 261–271, 399, 406, 450, 471–488, 560, 572, 622, 628, 663, 713–715, 729–742, 753, 789, 841–842, 940, 981, 1209, 1304–1307, 1351, 1353, 1444, 1548–1589, 1687, 1713, 1824, 2198–2200, 2262
   queue.py1711691%45, 53, 58, 105, 130–133, 173, 187, 205–208, 243, 267–270, 305, 330–333
   serializers.py350100% 
   tasks.py158715990%105, 119, 152, 188–189, 193–196, 198, 201, 251, 286–287, 407–408, 414, 417, 460–461, 502–507, 572, 578, 586, 765–766, 786–789, 865–866, 889, 904, 909, 914, 921, 927, 930–932, 935–938, 941–943, 963, 1016–1017, 1103–1104, 1108, 1132, 1186, 1298–1299, 1380–1381, 1383, 1575–1576, 1669–1670, 1673, 1699, 1719, 1730, 1741, 1753, 1764, 1767–1774, 1777–1785, 1788–1795, 1819, 1964, 1976–1985, 2027, 2030–2035, 2038–2042, 2211–2215, 2314–2315, 2521–2531, 2534, 2543–2548, 2582–2584, 2618–2621, 2689–2693, 2702, 2791–2796, 2809, 2824, 2839
   utils.py8903396%718–722, 867, 1186, 1202–1208, 1217–1218, 1944–1948, 1976–1980, 2008–2012, 2056–2060, 2114–2118, 2139, 2148, 2157, 2166–2167
   views.py1453377%127–129, 134–135, 141–142, 185–188, 258–288, 299–314, 327–331
apps/sage_intacct/errors
   errors.py20100% 
   helpers.py47198%80
apps/sage_intacct/exports
   ap_payments.py130100% 
   bills.py280100% 
   charge_card_transactions.py220100% 
   expense_reports.py280100% 
   helpers.py56296%68, 141
   journal_entries.py76889%177–180, 316–319
   reimbursements.py130100% 
apps/tasks
   models.py690100% 
   serializers.py60100% 
   views.py370100% 
apps/users
   helpers.py120100% 
   models.py530100% 
   views.py170100% 
apps/workspaces
   actions.py46980%65, 84–85, 103, 122–133, 147
   enums.py840100% 
   helpers.py80100% 
   models.py1760100% 
   permissions.py34974%33, 59–67
   serializers.py43198%54
   signals.py310100% 
   system_comments.py60100% 
   tasks.py175995%53–59, 187–194, 235, 267–268, 379
   utils.py90100% 
   views.py320997%107–109, 136–139, 665–666
apps/workspaces/apis/advanced_settings
   serializers.py80396%260, 263, 266
   triggers.py100100% 
   views.py110100% 
apps/workspaces/apis/errors
   serializers.py200100% 
   views.py150100% 
apps/workspaces/apis/export_settings
   helpers.py730100% 
   serializers.py103397%267, 270, 273
   triggers.py39295%32–33
   views.py110100% 
apps/workspaces/apis/import_settings
   serializers.py1411887%232–238, 243–249, 257–265, 283, 286, 314, 317–318, 326
   triggers.py47198%31
   views.py330100% 
fyle_integrations_imports
   dataclasses.py310100% 
   models.py230100% 
   queues.py34585%24, 99, 136, 148–149
   signals.py100100% 
   tasks.py1278136%69–111, 120–126, 139–208, 212–227, 252–255, 257, 259, 264
fyle_integrations_imports/modules
   base.py1881095%75, 91–92, 95, 126, 230–231, 237–238, 351
   categories.py1773381%80, 96, 99–100, 103–104, 272–273, 283, 296, 298, 342, 351–355, 364–410
   cost_centers.py961288%132–133, 152–153, 162, 178, 180, 226, 235–239
   expense_custom_fields.py121794%83–88, 247, 263, 265, 294
   merchants.py1062576%80–83, 111–129, 155–162, 179–180, 190, 203, 205, 235, 238–242
   projects.py1234067%88, 103–168, 188–189, 199, 223, 270, 284–288
   tax_groups.py180100% 
   webhook_attributes.py130695%98, 100–102, 213–214
workers
   actions.py210100% 
   helpers.py420100% 
   worker.py56591%44–45, 77–78, 125
TOTAL1084594991% 

Tests Skipped Failures Errors Time
784 0 💤 0 ❌ 0 🔥 1m 5s ⏱️

@github-actions
Copy link


Diff Coverage
Diff: origin/master..HEAD, staged and unstaged changes

apps/internal/tasks.py (100%)
apps/sage_intacct/models.py (100%)
apps/sage_intacct/tasks.py (92.5%): Missing lines 1767-1771,1777-1781,1788-1792
apps/workspaces/enums.py (100%)

Total: 253 lines
Missing: 15 lines
Coverage: 94%

Copy link
Contributor

@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 (2)
apps/sage_intacct/models.py (1)

1129-1167: Keep the billable flag consistent with the dimensions you persist

The new warning uses dimensions_values (post-allocation), but the saved billable flag still uses the pre-allocation customer_id/item_id variables. If allocation clears those dimensions, you can persist billable=True while customer_id/item_id are NULL, yet log “billable disabled.” Compute billable once from the same dimensions_values and reuse it here (and in the other lineitem creators).

Proposed fix
-            if lineitem.billable and not (dimensions_values['customer_id'] and dimensions_values['item_id']):
+            is_billable = bool(lineitem.billable and dimensions_values['customer_id'] and dimensions_values['item_id'])
+            if lineitem.billable and not is_billable:
                 missing_fields = []
                 if not dimensions_values['customer_id']:
                     missing_fields.append('customer_id')
                 if not dimensions_values['item_id']:
                     missing_fields.append('item_id')
                 add_system_comment(
                     system_comments=system_comments,
                     source=SystemCommentSourceEnum.CREATE_BILL_LINEITEMS,
                     intent=SystemCommentIntentEnum.BILLABLE_DISABLED,
                     entity_type=SystemCommentEntityTypeEnum.EXPENSE,
                     workspace_id=expense_group.workspace_id,
                     entity_id=lineitem.id,
                     reason=SystemCommentReasonEnum.BILLABLE_SET_TO_FALSE_MISSING_DIMENSIONS,
                     info={'missing_fields': missing_fields, 'original_billable': lineitem.billable}
                 )
 
             bill_lineitem_object, _ = BillLineitem.objects.update_or_create(
                 bill=bill,
                 expense_id=lineitem.id,
                 defaults={
@@
-                    'billable': lineitem.billable if customer_id and item_id else False,
+                    'billable': is_billable,
apps/sage_intacct/tasks.py (1)

891-963: Missing system_comments bulk creation in journal entry exception handlers.

Unlike create_expense_report, create_bill, and create_charge_card_transaction, the create_journal_entry function does not persist system_comments in its exception handlers. This means any collected comments (e.g., from lines 750-760 for ERROR state or 789-798 for fallback vendor) will be lost if the export fails.

🐛 Proposed fix - add bulk create to each exception handler

For example, in the BulkError handler:

     except BulkError as exception:
+        if system_comments:
+            for comment in system_comments:
+                comment['workspace_id'] = expense_group.workspace_id
+                comment['export_type'] = ExportTypeEnum.JOURNAL_ENTRY
+            SystemComment.bulk_create_comments(system_comments)
         logger.info(exception.response)
         detail = exception.response

Apply the same pattern to all other exception handlers (WrongParamsError, NoPrivilegeError, InvalidTokenError, ValueErrorWithResponse, IntacctRESTBadRequestError, IntacctRESTInvalidTokenError, IntacctRESTInternalServerError, and Exception).

🤖 Fix all issues with AI agents
In `@tests/conftest.py`:
- Around line 350-372: The fixture get_or_create_task_log creates TaskLog
instances but doesn't apply a provided updated_at because TaskLog.updated_at
uses auto_now=True which ignores create() values; modify the
_get_or_create_task_log function so that if updated_at is provided and a TaskLog
was just created (or exists), set task_log.updated_at = updated_at and call
task_log.save(update_fields=["updated_at"]) (referencing TaskLog and
_get_or_create_task_log) to persist the historical timestamp used by tests.

In `@tests/test_system_comments/test_system_comments.py`:
- Around line 993-998: The mock for Schedule.objects.filter needs to support
chaining to .first()—instead of returning None from .filter(), have the patched
return value provide a .first() method (e.g. return a MagicMock whose first()
returns None or the desired object); update the mocker.patch call that targets
Schedule.objects.filter so it returns a MagicMock with first configured
(referencing Schedule.objects.filter and its .first() call) so the test no
longer raises AttributeError.
🧹 Nitpick comments (2)
apps/sage_intacct/tasks.py (2)

1095-1099: Consider extracting repeated system comment persistence logic into a helper.

The same 5-line block for persisting system comments is repeated in the success path and every exception handler across create_expense_report, create_bill, and create_charge_card_transaction (30+ times total). This could be simplified with a helper function.

♻️ Suggested helper function
def persist_system_comments(system_comments: list, workspace_id: int, export_type: ExportTypeEnum) -> None:
    """Persist collected system comments with workspace and export type context."""
    if system_comments:
        for comment in system_comments:
            comment['workspace_id'] = workspace_id
            comment['export_type'] = export_type
        SystemComment.bulk_create_comments(system_comments)

Then each handler becomes a single call:

persist_system_comments(system_comments, expense_group.workspace_id, ExportTypeEnum.EXPENSE_REPORT)

1852-1852: Type hint should use explicit | None instead of implicit Optional.

Per PEP 484 and the static analysis hint (RUF013), the parameter type should explicitly indicate it can be None.

-def validate_for_skipping_payment(export_module: Bill | ExpenseReport, workspace_id: int, type: str, system_comments: list = None) -> bool:
+def validate_for_skipping_payment(export_module: Bill | ExpenseReport, workspace_id: int, type: str, system_comments: list | None = None) -> bool:

Comment on lines +350 to +372
@pytest.fixture
def get_or_create_task_log(db) -> Callable:
"""
Fixture to get or create a TaskLog for an expense group
Returns a function that can be called with expense_group and optional parameters
"""
def _get_or_create_task_log(
expense_group: ExpenseGroup,
task_type: str = 'FETCHING_EXPENSES',
status: str = 'COMPLETE',
updated_at: Optional[datetime] = None
) -> TaskLog:
task_log = TaskLog.objects.filter(expense_group_id=expense_group.id).first()
if not task_log:
task_log = TaskLog.objects.create(
expense_group_id=expense_group.id,
workspace_id=expense_group.workspace_id,
type=task_type,
status=status,
updated_at=updated_at
)
return task_log
return _get_or_create_task_log
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "*.py" | xargs grep -l "class TaskLog" | head -5

Repository: fylein/fyle-intacct-api

Length of output: 117


🏁 Script executed:

cat ./apps/tasks/models.py

Repository: fylein/fyle-intacct-api

Length of output: 6929


🏁 Script executed:

grep -n "get_or_create_task_log" tests/conftest.py -A 5 -B 2

Repository: fylein/fyle-intacct-api

Length of output: 642


🏁 Script executed:

grep -r "get_or_create_task_log" tests/ --include="*.py" -n

Repository: fylein/fyle-intacct-api

Length of output: 7416


🏁 Script executed:

sed -n '976,1000p' tests/test_system_comments/test_system_comments.py

Repository: fylein/fyle-intacct-api

Length of output: 1206


🏁 Script executed:

sed -n '1756,1775p' tests/test_system_comments/test_system_comments.py

Repository: fylein/fyle-intacct-api

Length of output: 734


Handle updated_at after TaskLog creation since auto_now=True prevents passing it to create()

TaskLog.updated_at uses auto_now=True, so values passed to create() are silently ignored. Tests relying on historical timestamps (e.g., stuck exports detection) will use current time instead. Update the timestamp after creation when provided.

     def _get_or_create_task_log(
         expense_group: ExpenseGroup,
         task_type: str = 'FETCHING_EXPENSES',
         status: str = 'COMPLETE',
         updated_at: Optional[datetime] = None
     ) -> TaskLog:
         task_log = TaskLog.objects.filter(expense_group_id=expense_group.id).first()
         if not task_log:
             task_log = TaskLog.objects.create(
                 expense_group_id=expense_group.id,
                 workspace_id=expense_group.workspace_id,
                 type=task_type,
                 status=status,
                 updated_at=updated_at
             )
+        if updated_at is not None:
+            TaskLog.objects.filter(id=task_log.id).update(updated_at=updated_at)
+            task_log.refresh_from_db()
         return task_log
🧰 Tools
🪛 Ruff (0.14.14)

351-351: Unused function argument: db

(ARG001)

🤖 Prompt for AI Agents
In `@tests/conftest.py` around lines 350 - 372, The fixture get_or_create_task_log
creates TaskLog instances but doesn't apply a provided updated_at because
TaskLog.updated_at uses auto_now=True which ignores create() values; modify the
_get_or_create_task_log function so that if updated_at is provided and a TaskLog
was just created (or exists), set task_log.updated_at = updated_at and call
task_log.save(update_fields=["updated_at"]) (referencing TaskLog and
_get_or_create_task_log) to persist the historical timestamp used by tests.

Comment on lines +993 to +998
captured_comments = []
mocker.patch('apps.internal.tasks.export_to_intacct')
mocker.patch('apps.internal.tasks.update_failed_expenses')
mocker.patch('apps.internal.tasks.post_accounting_export_summary')
mocker.patch('apps.internal.tasks.Schedule.objects.filter', return_value=mocker.MagicMock(filter=mocker.MagicMock(return_value=None)))
mocker.patch.object(SystemComment, 'bulk_create_comments', side_effect=lambda comments: captured_comments.extend(comments))
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the test file and the surrounding context
cat -n tests/test_system_comments/test_system_comments.py | sed -n '990,1010p'

Repository: fylein/fyle-intacct-api

Length of output: 1401


🏁 Script executed:

# Let's get more context around the test function
cat -n tests/test_system_comments/test_system_comments.py | sed -n '970,1020p'

Repository: fylein/fyle-intacct-api

Length of output: 2910


🏁 Script executed:

# Let's find the test function name to understand what's being tested
cat -n tests/test_system_comments/test_system_comments.py | sed -n '950,1000p'

Repository: fylein/fyle-intacct-api

Length of output: 2951


🏁 Script executed:

# Find the retrigger_stuck_exports function
rg -n "def retrigger_stuck_exports" --type py

Repository: fylein/fyle-intacct-api

Length of output: 131


🏁 Script executed:

# Look for usage of Schedule.objects.filter in the codebase
rg -n "Schedule\.objects\.filter" --type py -A 3

Repository: fylein/fyle-intacct-api

Length of output: 18382


🏁 Script executed:

# Get the retrigger_stuck_exports function implementation
cat -n apps/internal/tasks.py | sed -n '38,200p'

Repository: fylein/fyle-intacct-api

Length of output: 8262


🏁 Script executed:

# Verify the test actually fails with current mock by checking if there are any other test runs
# Also check if this is the only place this mock pattern appears
rg -n "Schedule.objects.filter.*return_value.*MagicMock.*filter.*MagicMock.*return_value.*None" tests/

Repository: fylein/fyle-intacct-api

Length of output: 260


🏁 Script executed:

# Check line 1777 which also has a Schedule.objects.filter mock
cat -n tests/test_system_comments/test_system_comments.py | sed -n '1770,1800p'

Repository: fylein/fyle-intacct-api

Length of output: 1897


Fix mock to support .filter(...).first() chaining

The current mock returns None from .filter(), which causes .first() to raise AttributeError. Set up the mock to properly chain these calls.

Proposed fix
-    mocker.patch('apps.internal.tasks.Schedule.objects.filter', return_value=mocker.MagicMock(filter=mocker.MagicMock(return_value=None)))
+    mock_schedule_qs = mocker.MagicMock()
+    mock_schedule_filter = mocker.MagicMock()
+    mock_schedule_filter.first.return_value = None
+    mock_schedule_qs.filter.return_value = mock_schedule_filter
+    mocker.patch('apps.internal.tasks.Schedule.objects.filter', return_value=mock_schedule_qs)
🤖 Prompt for AI Agents
In `@tests/test_system_comments/test_system_comments.py` around lines 993 - 998,
The mock for Schedule.objects.filter needs to support chaining to
.first()—instead of returning None from .filter(), have the patched return value
provide a .first() method (e.g. return a MagicMock whose first() returns None or
the desired object); update the mocker.patch call that targets
Schedule.objects.filter so it returns a MagicMock with first configured
(referencing Schedule.objects.filter and its .first() call) so the test no
longer raises AttributeError.

@github-actions
Copy link

Coverage

Coverage Report
FileStmtsMissCoverMissing
apps
   exceptions.py471177%27, 33, 39–44, 50, 56–61, 67, 73, 79–80
apps/fyle
   actions.py124794%214, 263, 319–323
   constants.py10100% 
   helpers.py2351494%50–57, 66, 376, 470, 477–480, 483–484
   models.py3382194%315–317, 321–323, 327–329, 333–335, 339–341, 345–347, 408, 530–551, 574, 591
   queue.py500100% 
   serializers.py410100% 
   signals.py35683%22, 55–56, 69–71
   tasks.py5468784%132, 143–144, 147–148, 151–152, 174–175, 231–251, 263–268, 311–314, 339–343, 353–361, 400–410, 525–585, 906–910, 1048, 1059–1060, 1085–1086
   views.py1721591%84–91, 145, 245–246, 290–291, 316–320, 413–417
apps/internal
   actions.py39295%26–27
   helpers.py361364%18–35
   serializers.py300100% 
   tasks.py903264%31–35, 61–66, 109, 121–172
   views.py83199%39
apps/internal/services
   e2e_setup.py590100% 
   fixture_factory.py59395%218–220
apps/mappings
   constants.py20100% 
   exceptions.py71692%69–71, 79–81
   helpers.py300100% 
   models.py500100% 
   schedules.py150100% 
   serializers.py100100% 
   signals.py1111487%90–92, 127, 179–184, 190–194, 200–203, 209–210
   tasks.py1732983%117–118, 141–142, 145, 209–226, 254, 257, 260, 266, 269, 275, 314–316, 327, 399
   utils.py340100% 
   views.py46491%34–35, 114–115
apps/sage_intacct
   actions.py29486%21–22, 49–50
   connector.py8052597%133, 309, 321, 327, 403, 1414, 1713, 1840–1841, 1892–1893, 1914–1915, 1944–1945, 1953–1964, 1996–1997, 2017–2018
   dependent_fields.py2904286%345, 381–384, 413–422, 424–425, 427–429, 431–432, 434–435, 437–438, 442–444, 488, 508, 548–550, 596–598, 646–652, 656–657
   enums.py250100% 
   helpers.py56689%38–41, 58, 95
   models.py9886793%78–79, 261–271, 399, 406, 450, 471–488, 560, 572, 622, 628, 663, 713–715, 729–742, 753, 789, 841–842, 940, 981, 1209, 1304–1307, 1351, 1353, 1444, 1548–1589, 1687, 1713, 1824, 2198–2200, 2262
   queue.py1711691%45, 53, 58, 105, 130–133, 173, 187, 205–208, 243, 267–270, 305, 330–333
   serializers.py350100% 
   tasks.py144714490%105, 119, 152, 188–189, 193–196, 198, 201, 251, 286–287, 407–408, 414, 417, 460–461, 502–507, 572, 578, 586, 765–766, 786–789, 865–866, 889, 904, 909, 914, 921, 927, 930–932, 935–938, 941–943, 963, 1016–1017, 1097–1098, 1102, 1121, 1155, 1254–1255, 1330–1331, 1333, 1487–1488, 1575–1576, 1579, 1600, 1615, 1621, 1627, 1634, 1640, 1643–1645, 1648–1651, 1654–1656, 1682, 1827, 1839–1848, 1890, 1893–1898, 1901–1905, 2074–2078, 2177–2178, 2384–2394, 2397, 2406–2411, 2445–2447, 2481–2484, 2552–2556, 2565, 2654–2659, 2672, 2687, 2702
   utils.py8903396%718–722, 867, 1186, 1202–1208, 1217–1218, 1944–1948, 1976–1980, 2008–2012, 2056–2060, 2114–2118, 2139, 2148, 2157, 2166–2167
   views.py1453377%127–129, 134–135, 141–142, 185–188, 258–288, 299–314, 327–331
apps/sage_intacct/errors
   errors.py20100% 
   helpers.py47198%80
apps/sage_intacct/exports
   ap_payments.py130100% 
   bills.py280100% 
   charge_card_transactions.py220100% 
   expense_reports.py280100% 
   helpers.py56296%68, 141
   journal_entries.py76889%177–180, 316–319
   reimbursements.py130100% 
apps/tasks
   models.py690100% 
   serializers.py60100% 
   views.py370100% 
apps/users
   helpers.py120100% 
   models.py530100% 
   views.py170100% 
apps/workspaces
   actions.py46980%65, 84–85, 103, 122–133, 147
   enums.py840100% 
   helpers.py80100% 
   models.py1760100% 
   permissions.py34974%33, 59–67
   serializers.py43198%54
   signals.py310100% 
   system_comments.py60100% 
   tasks.py175995%53–59, 187–194, 235, 267–268, 379
   utils.py90100% 
   views.py320997%107–109, 136–139, 665–666
apps/workspaces/apis/advanced_settings
   serializers.py80396%260, 263, 266
   triggers.py100100% 
   views.py110100% 
apps/workspaces/apis/errors
   serializers.py200100% 
   views.py150100% 
apps/workspaces/apis/export_settings
   helpers.py730100% 
   serializers.py103397%267, 270, 273
   triggers.py39295%32–33
   views.py110100% 
apps/workspaces/apis/import_settings
   serializers.py1411887%232–238, 243–249, 257–265, 283, 286, 314, 317–318, 326
   triggers.py47198%31
   views.py330100% 
fyle_integrations_imports
   dataclasses.py310100% 
   models.py230100% 
   queues.py34585%24, 99, 136, 148–149
   signals.py100100% 
   tasks.py1278136%69–111, 120–126, 139–208, 212–227, 252–255, 257, 259, 264
fyle_integrations_imports/modules
   base.py1881095%75, 91–92, 95, 126, 230–231, 237–238, 351
   categories.py1773381%80, 96, 99–100, 103–104, 272–273, 283, 296, 298, 342, 351–355, 364–410
   cost_centers.py961288%132–133, 152–153, 162, 178, 180, 226, 235–239
   expense_custom_fields.py121794%83–88, 247, 263, 265, 294
   merchants.py1062576%80–83, 111–129, 155–162, 179–180, 190, 203, 205, 235, 238–242
   projects.py1234067%88, 103–168, 188–189, 199, 223, 270, 284–288
   tax_groups.py180100% 
   webhook_attributes.py130695%98, 100–102, 213–214
workers
   actions.py210100% 
   helpers.py420100% 
   worker.py56591%44–45, 77–78, 125
TOTAL1070593491% 

Tests Skipped Failures Errors Time
784 0 💤 0 ❌ 0 🔥 1m 4s ⏱️

@github-actions
Copy link


Diff Coverage
Diff: origin/master..HEAD, staged and unstaged changes

apps/internal/tasks.py (100%)
apps/sage_intacct/models.py (100%)
apps/sage_intacct/tasks.py (100%)
apps/workspaces/enums.py (100%)

Total: 113 lines
Missing: 0 lines
Coverage: 100%

Copy link
Contributor

@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: 1

🤖 Fix all issues with AI agents
In `@apps/sage_intacct/tasks.py`:
- Around line 746-761: system_comments are being accumulated via
add_system_comment but the bulk-create is only executed on the success path of
the Intacct journal-entry export, so any exception after accumulation causes
comments to be lost; locate where system_comments is collected (look for
variables named system_comments and calls to add_system_comment in the
journal-entry export flow for expense_group) and move the
persistence/bulk-create call for those system_comments into a finally block (or
ensure it's executed in finally) so comments are always saved regardless of
success or exception; apply the same change to the other similar blocks
referenced (around the ranges that add_system_comment at the other spots).

Comment on lines 746 to +761
in_progress_expenses = []
system_comments = []
# Don't include expenses with previous export state as ERROR and it's an auto import/export run
if not (is_auto_export and expense_group.expenses.first().previous_export_state == 'ERROR'):
if is_auto_export and expense_group.expenses.first().previous_export_state == 'ERROR':
add_system_comment(
system_comments=system_comments,
source=SystemCommentSourceEnum.CREATE_JOURNAL_ENTRY,
intent=SystemCommentIntentEnum.EXPORT_SUMMARY_NOT_UPDATED,
entity_type=SystemCommentEntityTypeEnum.EXPENSE_GROUP,
workspace_id=expense_group.workspace_id,
entity_id=expense_group.id,
export_type=ExportTypeEnum.JOURNAL_ENTRY,
reason=SystemCommentReasonEnum.EXPORT_SUMMARY_NOT_UPDATED_ERROR_STATE,
info={'previous_export_state': 'ERROR', 'is_auto_export': True}
)
else:
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Persist system comments even when journal-entry export fails.

system_comments can be populated before the Intacct post, but the bulk-create runs only on the success path. Any exception after comment accumulation drops those comments (unlike other export flows that use finally). Please move the bulk-create to a finally block so comments are always persisted.

🛠️ Proposed fix
-        if system_comments:
-            for comment in system_comments:
-                comment['workspace_id'] = expense_group.workspace_id
-                comment['export_type'] = ExportTypeEnum.JOURNAL_ENTRY
-            SystemComment.bulk_create_comments(system_comments)
+    finally:
+        if system_comments:
+            for comment in system_comments:
+                comment['workspace_id'] = expense_group.workspace_id
+                comment['export_type'] = ExportTypeEnum.JOURNAL_ENTRY
+            SystemComment.bulk_create_comments(system_comments)

Also applies to: 789-798, 816-816, 857-862

🤖 Prompt for AI Agents
In `@apps/sage_intacct/tasks.py` around lines 746 - 761, system_comments are being
accumulated via add_system_comment but the bulk-create is only executed on the
success path of the Intacct journal-entry export, so any exception after
accumulation causes comments to be lost; locate where system_comments is
collected (look for variables named system_comments and calls to
add_system_comment in the journal-entry export flow for expense_group) and move
the persistence/bulk-create call for those system_comments into a finally block
(or ensure it's executed in finally) so comments are always saved regardless of
success or exception; apply the same change to the other similar blocks
referenced (around the ranges that add_system_comment at the other spots).

@github-actions
Copy link

Coverage

Coverage Report
FileStmtsMissCoverMissing
apps
   exceptions.py471177%27, 33, 39–44, 50, 56–61, 67, 73, 79–80
apps/fyle
   actions.py124794%214, 263, 319–323
   constants.py10100% 
   helpers.py2351494%50–57, 66, 376, 470, 477–480, 483–484
   models.py3382194%315–317, 321–323, 327–329, 333–335, 339–341, 345–347, 408, 530–551, 574, 591
   queue.py500100% 
   serializers.py410100% 
   signals.py35683%22, 55–56, 69–71
   tasks.py5468784%132, 143–144, 147–148, 151–152, 174–175, 231–251, 263–268, 311–314, 339–343, 353–361, 400–410, 525–585, 906–910, 1048, 1059–1060, 1085–1086
   views.py1721591%84–91, 145, 245–246, 290–291, 316–320, 413–417
apps/internal
   actions.py39295%26–27
   helpers.py361364%18–35
   serializers.py300100% 
   tasks.py903264%31–35, 61–66, 109, 121–172
   views.py83199%39
apps/internal/services
   e2e_setup.py590100% 
   fixture_factory.py59395%218–220
apps/mappings
   constants.py20100% 
   exceptions.py71692%69–71, 79–81
   helpers.py300100% 
   models.py500100% 
   schedules.py150100% 
   serializers.py100100% 
   signals.py1111487%90–92, 127, 179–184, 190–194, 200–203, 209–210
   tasks.py1732983%117–118, 141–142, 145, 209–226, 254, 257, 260, 266, 269, 275, 314–316, 327, 399
   utils.py340100% 
   views.py46491%34–35, 114–115
apps/sage_intacct
   actions.py29486%21–22, 49–50
   connector.py8052597%133, 309, 321, 327, 403, 1414, 1713, 1840–1841, 1892–1893, 1914–1915, 1944–1945, 1953–1964, 1996–1997, 2017–2018
   dependent_fields.py2904286%345, 381–384, 413–422, 424–425, 427–429, 431–432, 434–435, 437–438, 442–444, 488, 508, 548–550, 596–598, 646–652, 656–657
   enums.py250100% 
   helpers.py56689%38–41, 58, 95
   models.py9886793%78–79, 261–271, 399, 406, 450, 471–488, 560, 572, 622, 628, 663, 713–715, 729–742, 753, 789, 841–842, 940, 981, 1209, 1304–1307, 1351, 1353, 1444, 1548–1589, 1687, 1713, 1824, 2198–2200, 2262
   queue.py1711691%45, 53, 58, 105, 130–133, 173, 187, 205–208, 243, 267–270, 305, 330–333
   serializers.py350100% 
   tasks.py144714490%105, 119, 152, 188–189, 193–196, 198, 201, 251, 286–287, 407–408, 414, 417, 460–461, 502–507, 572, 578, 586, 765–766, 786–789, 865–866, 889, 904, 909, 914, 921, 927, 930–932, 935–938, 941–943, 963, 1016–1017, 1097–1098, 1102, 1121, 1155, 1254–1255, 1330–1331, 1333, 1487–1488, 1575–1576, 1579, 1600, 1615, 1621, 1627, 1634, 1640, 1643–1645, 1648–1651, 1654–1656, 1682, 1827, 1839–1848, 1890, 1893–1898, 1901–1905, 2074–2078, 2177–2178, 2384–2394, 2397, 2406–2411, 2445–2447, 2481–2484, 2552–2556, 2565, 2654–2659, 2672, 2687, 2702
   utils.py8903396%718–722, 867, 1186, 1202–1208, 1217–1218, 1944–1948, 1976–1980, 2008–2012, 2056–2060, 2114–2118, 2139, 2148, 2157, 2166–2167
   views.py1453377%127–129, 134–135, 141–142, 185–188, 258–288, 299–314, 327–331
apps/sage_intacct/errors
   errors.py20100% 
   helpers.py47198%80
apps/sage_intacct/exports
   ap_payments.py130100% 
   bills.py280100% 
   charge_card_transactions.py220100% 
   expense_reports.py280100% 
   helpers.py56296%68, 141
   journal_entries.py76889%177–180, 316–319
   reimbursements.py130100% 
apps/tasks
   models.py690100% 
   serializers.py60100% 
   views.py370100% 
apps/users
   helpers.py120100% 
   models.py530100% 
   views.py170100% 
apps/workspaces
   actions.py46980%65, 84–85, 103, 122–133, 147
   enums.py840100% 
   helpers.py80100% 
   models.py1760100% 
   permissions.py34974%33, 59–67
   serializers.py43198%54
   signals.py310100% 
   system_comments.py60100% 
   tasks.py175995%53–59, 187–194, 235, 267–268, 379
   utils.py90100% 
   views.py320997%107–109, 136–139, 665–666
apps/workspaces/apis/advanced_settings
   serializers.py80396%260, 263, 266
   triggers.py100100% 
   views.py110100% 
apps/workspaces/apis/errors
   serializers.py200100% 
   views.py150100% 
apps/workspaces/apis/export_settings
   helpers.py730100% 
   serializers.py103397%267, 270, 273
   triggers.py39295%32–33
   views.py110100% 
apps/workspaces/apis/import_settings
   serializers.py1411887%232–238, 243–249, 257–265, 283, 286, 314, 317–318, 326
   triggers.py47198%31
   views.py330100% 
fyle_integrations_imports
   dataclasses.py310100% 
   models.py230100% 
   queues.py34585%24, 99, 136, 148–149
   signals.py100100% 
   tasks.py1278136%69–111, 120–126, 139–208, 212–227, 252–255, 257, 259, 264
fyle_integrations_imports/modules
   base.py1881095%75, 91–92, 95, 126, 230–231, 237–238, 351
   categories.py1773381%80, 96, 99–100, 103–104, 272–273, 283, 296, 298, 342, 351–355, 364–410
   cost_centers.py961288%132–133, 152–153, 162, 178, 180, 226, 235–239
   expense_custom_fields.py121794%83–88, 247, 263, 265, 294
   merchants.py1062576%80–83, 111–129, 155–162, 179–180, 190, 203, 205, 235, 238–242
   projects.py1234067%88, 103–168, 188–189, 199, 223, 270, 284–288
   tax_groups.py180100% 
   webhook_attributes.py130695%98, 100–102, 213–214
workers
   actions.py210100% 
   helpers.py420100% 
   worker.py56591%44–45, 77–78, 125
TOTAL1070593491% 

Tests Skipped Failures Errors Time
784 0 💤 0 ❌ 0 🔥 1m 8s ⏱️

@github-actions
Copy link


Diff Coverage
Diff: origin/master..HEAD, staged and unstaged changes

apps/internal/tasks.py (100%)
apps/sage_intacct/models.py (100%)
apps/sage_intacct/tasks.py (100%)
apps/workspaces/enums.py (100%)

Total: 113 lines
Missing: 0 lines
Coverage: 100%

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