Skip to content

feat: Enhance Sage Intacct credential handling #699

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jun 2, 2025
Merged

Conversation

ruuushhh
Copy link
Contributor

@ruuushhh ruuushhh commented May 28, 2025

Description

  • Introduced invalidate_sage_intacct_credentials utility to manage expired credentials.
  • Updated exception handling in various modules to utilize the new utility for InvalidTokenError.
  • Added is_expired field to SageIntacctCredential model to track credential status.
  • Adjusted logging and error messages for clarity and consistency across the application.
  • Added migration for the new is_expired field in the database.

Clickup

https://app.clickup.com/

Summary by CodeRabbit

  • New Features

    • Added a new indicator to track if Sage Intacct credentials have expired.
    • Introduced a utility to explicitly invalidate expired or invalid Sage Intacct credentials.
    • Added a method to retrieve only active (non-expired) Sage Intacct credentials.
  • Bug Fixes

    • Improved error messages for missing or invalid Sage Intacct credentials, providing clearer feedback.
    • Credentials are now explicitly invalidated upon token errors to prevent further issues.
    • Enhanced exception handling by separating missing credentials and invalid token errors for better control and logging.
    • Made credential retrieval more robust by using a dedicated method that filters out expired credentials.
  • Tests

    • Updated and expanded test cases to verify new error messages, exception handling, and credential invalidation logic.
    • Added tests for the credential invalidation utility function, email sending utility, and various helper functions.
    • Improved coverage for exception scenarios in Sage Intacct reimbursement, credit card, and journal entry processing.
    • Added new unit tests for helper functions, API helpers, and import modules.
  • Chores

    • Updated database schema and fixtures to include the new credential expiry field.

- Introduced `invalidate_sage_intacct_credentials` utility to manage expired credentials.
- Updated exception handling in various modules to utilize the new utility for `InvalidTokenError`.
- Added `is_expired` field to `SageIntacctCredential` model to track credential status.
- Adjusted logging and error messages for clarity and consistency across the application.
- Added migration for the new `is_expired` field in the database.
Copy link
Contributor

coderabbitai bot commented May 28, 2025

Walkthrough

The changes introduce explicit handling and invalidation of expired or invalid Sage Intacct credentials across exception handlers in multiple modules. A new is_expired field is added to the SageIntacctCredential model, along with a utility function to invalidate credentials. Exception messages and test assertions are updated for clarity and specificity, and a database migration supports the new field.

Changes

File(s) Change Summary
apps/mappings/exceptions.py, apps/mappings/tasks.py, apps/sage_intacct/dependent_fields.py, apps/sage_intacct/tasks.py, apps/sage_intacct/views.py Exception handling for SageIntacctCredential.DoesNotExist and InvalidTokenError split into separate blocks; explicit credential invalidation and clearer error messages/logging added.
apps/sage_intacct/helpers.py patch_integration_settings signature updated: errors default changed to None, new is_token_expired parameter; payload construction adjusted.
apps/workspaces/migrations/0045_sageintacctcredential_is_expired.py, apps/workspaces/models.py Added is_expired BooleanField to SageIntacctCredential model; migration created.
fyle_intacct_api/utils.py Added invalidate_sage_intacct_credentials utility to mark credentials as expired and update integration settings.
tests/sql_fixtures/reset_db_fixtures/reset_db.sql Database schema updated for is_expired column; migration and sequence records updated.
tests/test_mappings/test_exceptions.py, tests/test_sageintacct/test_views.py Updated test assertions for new, more specific error messages.
tests/test_sageintacct/test_tasks.py Added tests for new exception handling scenarios in reimbursement task; verifies task log status on exceptions.
tests/test_sageintacct/test_utils.py Added test for invalidate_sage_intacct_credentials utility covering credential expiry scenarios and patch calls.
tests/test_workspaces/test_utils.py Added tests for send_email utility function with SendGrid client mocking.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant View/Task
    participant SageIntacctCredential
    participant Utils
    participant IntegrationSettings

    User->>View/Task: Initiate Sage Intacct operation
    View/Task->>SageIntacctCredential: Retrieve credentials
    alt Credentials missing
        View/Task->>User: Return error (credentials missing)
    else Credentials found
        View/Task->>Sage Intacct: Perform operation
        alt InvalidTokenError raised
            View/Task->>Utils: invalidate_sage_intacct_credentials(workspace_id, credentials)
            Utils->>SageIntacctCredential: Set is_expired = True
            Utils->>IntegrationSettings: patch_integration_settings(is_token_expired=True)
            View/Task->>User: Return error (invalid token)
        else NoPrivilegeError raised
            View/Task->>User: Return error (no privilege)
        else Success
            View/Task->>User: Return success
        end
    end
Loading

Poem

With a hop and a skip, credentials we check,
If tokens expire, we mark them a wreck!
Now errors are clearer, logs easy to read,
Expired flags set when tokens mislead.
So here’s to exception handling, neat and precise—
This bunny’s code garden is looking quite nice!
🐇✨


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2caed13 and 67d06c3.

📒 Files selected for processing (1)
  • apps/workspaces/views.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/workspaces/views.py
✨ 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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions github-actions bot added the size/M Medium PR label May 28, 2025
Copy link

Coverage

Coverage Report
FileStmtsMissCoverMissing
apps
   exceptions.py471177%27, 33, 39–44, 50, 56–61, 67, 73, 79–80
apps/fyle
   actions.py117794%212, 261, 317–321
   constants.py10100% 
   helpers.py2492690%127, 210–245, 290, 582, 589–592, 595–596
   models.py3362393%313–315, 319–321, 325–327, 331–333, 337–339, 343–345, 406, 512–545, 568, 585
   queue.py280100% 
   serializers.py410100% 
   signals.py35683%23, 56–57, 70–72
   tasks.py1976666%164–169, 172–177, 180–185, 211–221, 233–238, 257, 281–286, 289–291, 295, 348–395
   views.py1881592%96–103, 271, 356–357, 390–391, 416–420, 513–517
apps/internal
   actions.py180100% 
   views.py40198%37
apps/mappings
   constants.py10100% 
   exceptions.py66395%69–71
   helpers.py20575%45–55
   models.py700100% 
   serializers.py100100% 
   signals.py84792%85–87, 122, 169–174
   tasks.py1071982%112–126, 245, 248, 251, 256–263
   utils.py360100% 
   views.py51296%35–36
apps/mappings/imports
   schedules.py150100% 
apps/mappings/imports/modules
   base.py153299%122, 345
   categories.py64592%112–113, 123, 125, 157
   cost_centers.py55591%91–92, 103, 105, 139
   expense_custom_fields.py69396%60–65
   merchants.py260100% 
   projects.py61493%94–95, 111, 147
   tax_groups.py150100% 
apps/sage_intacct
   dependent_fields.py2644085%307, 343–346, 375–384, 386–387, 389–391, 393–394, 396–397, 401–403, 447, 467, 507–509, 555–557, 605–611, 615–616
   helpers.py540100% 
   models.py7893596%70–71, 221, 328, 335, 379, 400–417, 476, 538, 544, 579, 749, 1066–1069, 1236–1256, 1346, 1778
   queue.py1901990%77–80, 126, 142, 150, 179–182, 244–247, 309–312
   serializers.py310100% 
   tasks.py10049391%100–101, 127–130, 164, 200–201, 205–208, 210, 213, 263, 297–298, 443, 449, 457, 612–613, 633–634, 680–681, 785–786, 853–854, 957–958, 1014–1015, 1116–1117, 1173–1174, 1302–1303, 1369–1378, 1420–1444, 1563–1567, 1623–1629, 1715–1725, 1728, 1762–1764, 1798–1801, 1863, 1872
   utils.py7886891%152, 382–383, 388, 401–402, 410–411, 474–475, 549–553, 657, 897–920, 1333–1355, 1378–1388, 1616–1620, 1648–1652, 1680–1684, 1726–1730, 1783–1787, 1807, 1816, 1825, 1834–1835, 1846, 1869–1871, 2028
   views.py1181686%193–195, 200–201, 207–208, 242–245, 269, 289–292
apps/sage_intacct/errors
   errors.py20100% 
   helpers.py26196%73
apps/tasks
   models.py670100% 
   serializers.py60100% 
   views.py370100% 
apps/users
   helpers.py120100% 
   models.py530100% 
   views.py240100% 
apps/workspaces
   actions.py46296%42, 126
   models.py1090100% 
   permissions.py34974%33, 59–67
   serializers.py39197%53
   signals.py26196%70
   tasks.py110397%179, 209–210
   utils.py9544%15–24
   views.py215797%69–72, 105, 429–430
apps/workspaces/apis/advanced_settings
   serializers.py78396%247, 250, 253
   triggers.py100100% 
   views.py110100% 
apps/workspaces/apis/errors
   serializers.py200100% 
   views.py150100% 
apps/workspaces/apis/export_settings
   serializers.py98397%256, 259, 262
   triggers.py27293%33–34
   views.py110100% 
apps/workspaces/apis/import_settings
   serializers.py1401788%231–237, 242–248, 256, 274, 277, 305, 308–309, 317
   triggers.py47198%31
   views.py330100% 
workers/export
   actions.py11191%20
   worker.py51884%39–41, 44–45, 73–74, 107
TOTAL680554592% 

Tests Skipped Failures Errors Time
317 0 💤 0 ❌ 0 🔥 39.276s ⏱️

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

🧹 Nitpick comments (1)
fyle_intacct_api/utils.py (1)

30-38: Enhance error handling and add input validation.

The function logic is correct, but consider improving robustness:

  1. Add workspace_id validation
  2. Handle potential database errors
  3. Consider returning success status for verification
 def invalidate_sage_intacct_credentials(workspace_id, sage_intacct_credentials=None):
+    if not workspace_id:
+        raise ValueError("workspace_id is required")
+        
     if not sage_intacct_credentials:
         sage_intacct_credentials = SageIntacctCredential.objects.filter(workspace_id=workspace_id, is_expired=False).first()

     if sage_intacct_credentials:
-        if not sage_intacct_credentials.is_expired:
-            patch_integration_settings(workspace_id, is_token_expired=True)
-        sage_intacct_credentials.is_expired = True
-        sage_intacct_credentials.save()
+        try:
+            if not sage_intacct_credentials.is_expired:
+                patch_integration_settings(workspace_id, is_token_expired=True)
+            sage_intacct_credentials.is_expired = True
+            sage_intacct_credentials.save()
+            return True
+        except Exception as e:
+            logger.error(f"Failed to invalidate credentials for workspace {workspace_id}: {e}")
+            return False
+    return False
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1c1172d and e146ef4.

📒 Files selected for processing (13)
  • apps/mappings/exceptions.py (2 hunks)
  • apps/mappings/tasks.py (2 hunks)
  • apps/sage_intacct/dependent_fields.py (2 hunks)
  • apps/sage_intacct/helpers.py (1 hunks)
  • apps/sage_intacct/tasks.py (8 hunks)
  • apps/sage_intacct/views.py (4 hunks)
  • apps/workspaces/migrations/0045_sageintacctcredential_is_expired.py (1 hunks)
  • apps/workspaces/models.py (1 hunks)
  • fyle_intacct_api/utils.py (2 hunks)
  • tests/sql_fixtures/reset_db_fixtures/reset_db.sql (5 hunks)
  • tests/test_mappings/test_exceptions.py (2 hunks)
  • tests/test_sageintacct/test_tasks.py (1 hunks)
  • tests/test_sageintacct/test_views.py (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
apps/sage_intacct/tasks.py (2)
fyle_intacct_api/utils.py (1)
  • invalidate_sage_intacct_credentials (30-38)
apps/workspaces/models.py (1)
  • SageIntacctCredential (166-181)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: pytest
🔇 Additional comments (32)
apps/workspaces/migrations/0045_sageintacctcredential_is_expired.py (1)

1-19: LGTM! Well-structured Django migration.

The migration correctly adds the is_expired BooleanField to the SageIntacctCredential model with appropriate default value and descriptive help text. The migration follows Django conventions and dependencies are properly set.

apps/workspaces/models.py (1)

175-175: LGTM! Appropriate field addition for credential expiry tracking.

The is_expired BooleanField is well-designed with a sensible default value of False and clear help text. This field will enable proper tracking of credential expiration status as part of the enhanced credential management system.

tests/test_sageintacct/test_views.py (2)

195-195: Improved error messaging specificity.

The updated error message now includes the specific workspace_id, making it easier to identify which workspace has missing credentials. This change aligns with the enhanced error handling improvements.


219-219: Consistent error messaging pattern.

The error message follows the same improved pattern as the sync_dimensions test, providing specific workspace identification for better debugging and user experience.

tests/test_mappings/test_exceptions.py (2)

79-79: Enhanced error specificity for InvalidTokenError.

The updated error message provides clear distinction between invalid token errors and missing credentials, improving debugging capabilities. The workspace_id inclusion helps identify the specific workspace affected.


91-91: Clear separation of credential existence vs validity errors.

The specific error message for missing credentials helps distinguish between scenarios where credentials don't exist versus when they're invalid/expired. This granular error handling supports better user experience and debugging.

fyle_intacct_api/utils.py (1)

3-4: LGTM! Clean import additions.

The imports are appropriately added for the new utility function.

apps/mappings/tasks.py (2)

13-13: LGTM! Import addition for credential invalidation utility.

The import is correctly added to support the new credential invalidation logic.


112-117:

✅ Verification successful

Excellent separation of exception handling logic.

The split exception handling provides better granularity:

  • SageIntacctCredential.DoesNotExist: Logs missing credentials
  • InvalidTokenError: Properly invalidates credentials before logging

This ensures proper cleanup of invalid tokens while maintaining clear error messaging.

Let me verify the consistency of this pattern across the codebase:


🏁 Script executed:

#!/bin/bash
# Description: Verify that invalidate_sage_intacct_credentials is called consistently for InvalidTokenError across the codebase
# Expected: All InvalidTokenError handlers should call the invalidation function

rg -A 2 -B 2 "except InvalidTokenError" --type py

Length of output: 5387


InvalidTokenError Cleanup Verified Across Sage Intacct Integration
All except InvalidTokenError blocks in the Sage Intacct code paths invoke invalidate_sage_intacct_credentials(...) before logging, ensuring consistent cleanup of invalid tokens:

  • apps/mappings/tasks.py
  • apps/mappings/exceptions.py
  • apps/sage_intacct/views.py
  • apps/sage_intacct/dependent_fields.py
  • apps/sage_intacct/tasks.py

Excellent work maintaining this pattern!

tests/test_sageintacct/test_tasks.py (1)

672-681: Great test coverage for enhanced exception scenarios.

The test properly covers the new exception handling patterns:

  • SageIntacctCredential.DoesNotExist: Verifies FAILED status
  • NoPrivilegeError: Verifies FAILED status

The test setup with updated_at timestamp manipulation is appropriate for simulating the exception scenarios.

apps/mappings/exceptions.py (2)

18-18: LGTM! Consistent import for credential invalidation.

The import follows the same pattern used across other modules in this PR.


52-61: Excellent consistency with refined exception handling.

The split exception handling matches the pattern established across the codebase:

  • SageIntacctCredential.DoesNotExist: Clear error messaging for missing credentials
  • InvalidTokenError: Proper credential invalidation followed by error logging

Both maintain appropriate alert levels (False) and status (FAILED) for these scenarios.

apps/sage_intacct/helpers.py (2)

117-117: Good improvement to function signature design.

Changing the default value of errors from 0 to None improves the function's design by making it clearer when no error count should be included in the payload. This aligns well with the conditional payload construction logic.


124-132: LGTM! Clean conditional payload construction.

The payload construction logic is well-designed:

  • The tpa_name is always included as a constant
  • errors_count and is_token_expired are only included when their values are not None

This approach prevents sending unnecessary or default values in the API payload, making the integration more efficient and cleaner.

apps/sage_intacct/dependent_fields.py (2)

12-12: Import addition supports the enhanced credential handling.

The import of invalidate_sage_intacct_credentials is correctly placed and necessary for the improved exception handling logic below.


385-391: Excellent refinement of exception handling logic.

The separation of exception handling is a significant improvement:

  1. SageIntacctCredential.DoesNotExist: Appropriately handles missing credentials with a specific error message
  2. InvalidTokenError: Correctly calls invalidate_sage_intacct_credentials(workspace_id) to mark credentials as expired, then logs the specific error

This change ensures that invalid tokens trigger the proper credential invalidation workflow, which aligns with the broader enhancement described in the PR objectives.

tests/sql_fixtures/reset_db_fixtures/reset_db.sql (4)

5-7: The version bump from pg_dump version 15.12 to 15.13 is purely informational and has no impact on the fixture’s behavior.


5963-5963: Confirm migration registration
The new record 0045_sageintacctcredential_is_expired is correctly appended to django_migrations. Ensure this lines up with your filesystem migration numbering.


9840-9841: Ensure COPY column alignment
The COPY statement now includes workspace_id and is_expired, matching the updated table schema. Verify the column order is correct to avoid data misalignment.


10003-10003: Update sequence to reflect new migration
The django_migrations_id_seq is properly set to 235 after adding the new migration entry.

apps/sage_intacct/views.py (4)

16-16: LGTM: Proper import addition.

The import for invalidate_sage_intacct_credentials is correctly added to support the new credential invalidation functionality.


223-223: LGTM: Credential assignment for exception handling.

The sage_intacct_credentials variable is properly assigned to be available for the InvalidTokenError exception handler. This ensures the variable is in scope when needed for credential invalidation.


234-250: LGTM: Improved granular exception handling.

The separation of SageIntacctCredential.DoesNotExist and InvalidTokenError into distinct exception blocks is an excellent improvement. This allows for:

  • Specific error messages for each scenario
  • Proper credential invalidation when tokens are invalid
  • Better logging and user feedback

The call to invalidate_sage_intacct_credentials is correctly placed and properly parameterized.


281-297: LGTM: Consistent exception handling pattern.

The exception handling pattern is consistently applied in the second POST method, maintaining the same structure and logic as the first method. This ensures uniform behavior across the API endpoints.

apps/sage_intacct/tasks.py (8)

14-14: LGTM: Consistent import addition.

The import for invalidate_sage_intacct_credentials is correctly added, maintaining consistency with the views module.


725-737: LGTM: Proper exception separation in journal entry creation.

The separation of NoPrivilegeError and InvalidTokenError is well-implemented:

  • NoPrivilegeError maintains existing error handling behavior
  • InvalidTokenError properly invalidates credentials before handling the error
  • Variable scope is correct as sage_intacct_credentials is available from line 622

900-911: LGTM: Consistent pattern in expense report creation.

The exception handling follows the same pattern as journal entry creation, ensuring consistent behavior across different export types.


1059-1070: LGTM: Consistent pattern in bill creation.

The exception handling maintains the same structure and logic, providing uniform credential invalidation behavior.


1219-1230: LGTM: Consistent pattern in charge card transaction creation.

The exception handling follows the established pattern, ensuring comprehensive coverage across all export types.


1423-1428: LGTM: Proper credential invalidation in AP payment creation.

The InvalidTokenError handling correctly invalidates credentials using the proper workspace ID from task_log.workspace_id and the credentials retrieved earlier in the function.


1554-1560: LGTM: Proper credential invalidation in reimbursement creation.

The exception handling follows the same pattern as AP payment creation, maintaining consistency in error handling behavior.


1623-1629: LGTM: Excellent exception handling restructure.

The updated exception handling in check_sage_intacct_object_status is well-structured:

  • Combines SageIntacctCredential.DoesNotExist and NoPrivilegeError as they both result in early return
  • Separates InvalidTokenError for proper credential invalidation
  • Variable scope is correct since InvalidTokenError can only occur after successful credential retrieval

Comment on lines +3609 to 3611
workspace_id integer NOT NULL,
is_expired boolean NOT NULL
);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add a default value for is_expired
To ensure new inserts without an explicit is_expired value default to false (matching the Django migration), update the column definition as follows:

-    is_expired boolean NOT NULL
+    is_expired boolean NOT NULL DEFAULT false
📝 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
workspace_id integer NOT NULL,
is_expired boolean NOT NULL
);
workspace_id integer NOT NULL,
is_expired boolean NOT NULL DEFAULT false
);
🤖 Prompt for AI Agents
In tests/sql_fixtures/reset_db_fixtures/reset_db.sql around lines 3609 to 3611,
the column is_expired lacks a default value, which can cause issues when
inserting rows without specifying this field. Modify the is_expired column
definition to include a default value of false by adding "DEFAULT false" to its
declaration, ensuring consistency with the Django migration and preventing null
or unexpected values.

@ruuushhh ruuushhh self-assigned this May 28, 2025
Copy link

Coverage

Coverage Report
FileStmtsMissCoverMissing
apps
   exceptions.py471177%27, 33, 39–44, 50, 56–61, 67, 73, 79–80
apps/fyle
   actions.py117794%212, 261, 317–321
   constants.py10100% 
   helpers.py2492690%127, 210–245, 290, 582, 589–592, 595–596
   models.py3362393%313–315, 319–321, 325–327, 331–333, 337–339, 343–345, 406, 512–545, 568, 585
   queue.py280100% 
   serializers.py410100% 
   signals.py35683%23, 56–57, 70–72
   tasks.py1976666%164–169, 172–177, 180–185, 211–221, 233–238, 257, 281–286, 289–291, 295, 348–395
   views.py1881592%96–103, 271, 356–357, 390–391, 416–420, 513–517
apps/internal
   actions.py180100% 
   views.py40198%37
apps/mappings
   constants.py10100% 
   exceptions.py66395%69–71
   helpers.py20575%45–55
   models.py700100% 
   serializers.py100100% 
   signals.py84792%85–87, 122, 169–174
   tasks.py1071982%112–126, 245, 248, 251, 256–263
   utils.py360100% 
   views.py51296%35–36
apps/mappings/imports
   schedules.py150100% 
apps/mappings/imports/modules
   base.py153299%122, 345
   categories.py64592%112–113, 123, 125, 157
   cost_centers.py55591%91–92, 103, 105, 139
   expense_custom_fields.py69396%60–65
   merchants.py260100% 
   projects.py61493%94–95, 111, 147
   tax_groups.py150100% 
apps/sage_intacct
   dependent_fields.py2644085%307, 343–346, 375–384, 386–387, 389–391, 393–394, 396–397, 401–403, 447, 467, 507–509, 555–557, 605–611, 615–616
   helpers.py540100% 
   models.py7893596%70–71, 221, 328, 335, 379, 400–417, 476, 538, 544, 579, 749, 1066–1069, 1236–1256, 1346, 1778
   queue.py1901990%77–80, 126, 142, 150, 179–182, 244–247, 309–312
   serializers.py310100% 
   tasks.py10049391%100–101, 127–130, 164, 200–201, 205–208, 210, 213, 263, 297–298, 443, 449, 457, 612–613, 633–634, 680–681, 785–786, 853–854, 957–958, 1014–1015, 1116–1117, 1173–1174, 1302–1303, 1369–1378, 1420–1444, 1563–1567, 1623–1629, 1715–1725, 1728, 1762–1764, 1798–1801, 1863, 1872
   utils.py7886891%152, 382–383, 388, 401–402, 410–411, 474–475, 549–553, 657, 897–920, 1333–1355, 1378–1388, 1616–1620, 1648–1652, 1680–1684, 1726–1730, 1783–1787, 1807, 1816, 1825, 1834–1835, 1846, 1869–1871, 2028
   views.py1181686%193–195, 200–201, 207–208, 242–245, 269, 289–292
apps/sage_intacct/errors
   errors.py20100% 
   helpers.py26196%73
apps/tasks
   models.py670100% 
   serializers.py60100% 
   views.py370100% 
apps/users
   helpers.py120100% 
   models.py530100% 
   views.py240100% 
apps/workspaces
   actions.py46296%42, 126
   models.py1090100% 
   permissions.py34974%33, 59–67
   serializers.py39197%53
   signals.py26196%70
   tasks.py110397%179, 209–210
   utils.py9544%15–24
   views.py215797%69–72, 105, 429–430
apps/workspaces/apis/advanced_settings
   serializers.py78396%247, 250, 253
   triggers.py100100% 
   views.py110100% 
apps/workspaces/apis/errors
   serializers.py200100% 
   views.py150100% 
apps/workspaces/apis/export_settings
   serializers.py98397%256, 259, 262
   triggers.py27293%33–34
   views.py110100% 
apps/workspaces/apis/import_settings
   serializers.py1401788%231–237, 242–248, 256, 274, 277, 305, 308–309, 317
   triggers.py47198%31
   views.py330100% 
workers/export
   actions.py11191%20
   worker.py51884%39–41, 44–45, 73–74, 107
TOTAL680554592% 

Tests Skipped Failures Errors Time
317 0 💤 0 ❌ 0 🔥 39.252s ⏱️

Copy link

codecov bot commented May 28, 2025

Codecov Report

Attention: Patch coverage is 69.33333% with 23 lines in your changes missing coverage. Please review.

Project coverage is 91.99%. Comparing base (980c3a8) to head (e146ef4).
Report is 39 commits behind head on master.

Files with missing lines Patch % Lines
apps/sage_intacct/tasks.py 78.78% 7 Missing ⚠️
apps/sage_intacct/views.py 57.14% 6 Missing ⚠️
apps/mappings/tasks.py 16.66% 5 Missing ⚠️
apps/sage_intacct/dependent_fields.py 37.50% 5 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #699      +/-   ##
==========================================
- Coverage   93.04%   91.99%   -1.06%     
==========================================
  Files          66       66              
  Lines        6515     6805     +290     
==========================================
+ Hits         6062     6260     +198     
- Misses        453      545      +92     
Files with missing lines Coverage Δ
apps/mappings/exceptions.py 95.45% <100.00%> (+0.45%) ⬆️
apps/sage_intacct/helpers.py 100.00% <100.00%> (ø)
apps/workspaces/models.py 100.00% <100.00%> (ø)
apps/mappings/tasks.py 82.24% <16.66%> (-3.90%) ⬇️
apps/sage_intacct/dependent_fields.py 84.84% <37.50%> (-0.98%) ⬇️
apps/sage_intacct/views.py 86.44% <57.14%> (-6.22%) ⬇️
apps/sage_intacct/tasks.py 90.73% <78.78%> (-0.09%) ⬇️

... and 4 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link

Coverage

Coverage Report
FileStmtsMissCoverMissing
apps
   exceptions.py471177%27, 33, 39–44, 50, 56–61, 67, 73, 79–80
apps/fyle
   actions.py117794%212, 261, 317–321
   constants.py10100% 
   helpers.py2492690%127, 210–245, 290, 582, 589–592, 595–596
   models.py3362393%313–315, 319–321, 325–327, 331–333, 337–339, 343–345, 406, 512–545, 568, 585
   queue.py280100% 
   serializers.py410100% 
   signals.py35683%23, 56–57, 70–72
   tasks.py1976666%164–169, 172–177, 180–185, 211–221, 233–238, 257, 281–286, 289–291, 295, 348–395
   views.py1881592%96–103, 271, 356–357, 390–391, 416–420, 513–517
apps/internal
   actions.py180100% 
   views.py40198%37
apps/mappings
   constants.py10100% 
   exceptions.py66395%69–71
   helpers.py20575%45–55
   models.py700100% 
   serializers.py100100% 
   signals.py84792%85–87, 122, 169–174
   tasks.py1071982%112–126, 245, 248, 251, 256–263
   utils.py360100% 
   views.py51296%35–36
apps/mappings/imports
   schedules.py150100% 
apps/mappings/imports/modules
   base.py153299%122, 345
   categories.py64592%112–113, 123, 125, 157
   cost_centers.py55591%91–92, 103, 105, 139
   expense_custom_fields.py69396%60–65
   merchants.py260100% 
   projects.py61493%94–95, 111, 147
   tax_groups.py150100% 
apps/sage_intacct
   dependent_fields.py2644085%307, 343–346, 375–384, 386–387, 389–391, 393–394, 396–397, 401–403, 447, 467, 507–509, 555–557, 605–611, 615–616
   helpers.py540100% 
   models.py7893596%70–71, 221, 328, 335, 379, 400–417, 476, 538, 544, 579, 749, 1066–1069, 1236–1256, 1346, 1778
   queue.py1901990%77–80, 126, 142, 150, 179–182, 244–247, 309–312
   serializers.py310100% 
   tasks.py10049391%100–101, 127–130, 164, 200–201, 205–208, 210, 213, 263, 297–298, 443, 449, 457, 612–613, 633–634, 680–681, 785–786, 853–854, 957–958, 1014–1015, 1116–1117, 1173–1174, 1302–1303, 1369–1378, 1420–1444, 1563–1567, 1623–1629, 1715–1725, 1728, 1762–1764, 1798–1801, 1863, 1872
   utils.py7886891%152, 382–383, 388, 401–402, 410–411, 474–475, 549–553, 657, 897–920, 1333–1355, 1378–1388, 1616–1620, 1648–1652, 1680–1684, 1726–1730, 1783–1787, 1807, 1816, 1825, 1834–1835, 1846, 1869–1871, 2028
   views.py1181686%193–195, 200–201, 207–208, 242–245, 269, 289–292
apps/sage_intacct/errors
   errors.py20100% 
   helpers.py26196%73
apps/tasks
   models.py670100% 
   serializers.py60100% 
   views.py370100% 
apps/users
   helpers.py120100% 
   models.py530100% 
   views.py240100% 
apps/workspaces
   actions.py46296%42, 126
   models.py1090100% 
   permissions.py34974%33, 59–67
   serializers.py39197%53
   signals.py26196%70
   tasks.py110397%179, 209–210
   utils.py9544%15–24
   views.py215797%69–72, 105, 429–430
apps/workspaces/apis/advanced_settings
   serializers.py78396%247, 250, 253
   triggers.py100100% 
   views.py110100% 
apps/workspaces/apis/errors
   serializers.py200100% 
   views.py150100% 
apps/workspaces/apis/export_settings
   serializers.py98397%256, 259, 262
   triggers.py27293%33–34
   views.py110100% 
apps/workspaces/apis/import_settings
   serializers.py1401788%231–237, 242–248, 256, 274, 277, 305, 308–309, 317
   triggers.py47198%31
   views.py330100% 
workers/export
   actions.py11191%20
   worker.py51884%39–41, 44–45, 73–74, 107
TOTAL680554592% 

Tests Skipped Failures Errors Time
318 0 💤 0 ❌ 0 🔥 40.268s ⏱️

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

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e146ef4 and a0c146b.

📒 Files selected for processing (1)
  • tests/test_sageintacct/test_utils.py (2 hunks)
🧰 Additional context used
🪛 Ruff (0.11.9)
tests/test_sageintacct/test_utils.py

1531-1531: Avoid equality comparisons to True; use if kwargs['is_token_expired']: for truth checks

Replace with kwargs['is_token_expired']

(E712)


1535-1535: Avoid equality comparisons to True; use if sage_intacct_credentials.is_expired: for truth checks

Replace with sage_intacct_credentials.is_expired

(E712)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: pytest
🔇 Additional comments (1)
tests/test_sageintacct/test_utils.py (1)

16-16: LGTM: Import statement correctly added.

The import for invalidate_sage_intacct_credentials is properly structured and necessary for the new test function.

Comment on lines +1502 to +1535
def test_invalidate_sage_intacct_credentials(mocker, db):
"""
Test invalidate sage intacct credentials
"""
workspace_id = 1
sage_intacct_credentials = SageIntacctCredential.objects.filter(workspace_id=workspace_id, is_expired=False).first()

mocked_patch = mocker.MagicMock()
mocker.patch('fyle_intacct_api.utils.patch_integration_settings', side_effect=mocked_patch)

# Should not fail if sage_intacct_credentials was not found
sage_intacct_credentials.delete()
invalidate_sage_intacct_credentials(workspace_id)
assert not mocked_patch.called

# Should not call patch_integration_settings if sage_intacct_credentials.is_expired is True
sage_intacct_credentials.is_expired = True
sage_intacct_credentials.save()
invalidate_sage_intacct_credentials(workspace_id)
assert not mocked_patch.called

# Should call patch_integration_settings with the correct arguments if sage_intacct_credentials.is_expired is False
sage_intacct_credentials.is_expired = False
sage_intacct_credentials.save()

invalidate_sage_intacct_credentials(workspace_id)

args, kwargs = mocked_patch.call_args
assert args[0] == workspace_id
assert kwargs['is_token_expired'] == True

# Verify the credentials were marked as expired
sage_intacct_credentials.refresh_from_db()
assert sage_intacct_credentials.is_expired == True
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

Fix logical error and improve code style.

The test has a critical logical error and some style issues:

  1. Critical Issue: After deleting the credential object on line 1513, the test tries to access it again on line 1517, which will cause an AttributeError.

  2. Style Issues: Lines 1531 and 1535 use explicit == True comparisons, which is not Pythonic.

Apply this diff to fix the logical error and style issues:

def test_invalidate_sage_intacct_credentials(mocker, db):
    """
    Test invalidate sage intacct credentials
    """
    workspace_id = 1
-    sage_intacct_credentials = SageIntacctCredential.objects.filter(workspace_id=workspace_id, is_expired=False).first()

    mocked_patch = mocker.MagicMock()
    mocker.patch('fyle_intacct_api.utils.patch_integration_settings', side_effect=mocked_patch)

    # Should not fail if sage_intacct_credentials was not found
+    sage_intacct_credentials = SageIntacctCredential.objects.filter(workspace_id=workspace_id, is_expired=False).first()
    sage_intacct_credentials.delete()
    invalidate_sage_intacct_credentials(workspace_id)
    assert not mocked_patch.called

    # Should not call patch_integration_settings if sage_intacct_credentials.is_expired is True
+    sage_intacct_credentials = SageIntacctCredential.objects.create(
+        workspace_id=workspace_id,
+        si_user_id='test_user',
+        si_company_id='test_company',
+        si_user_password='test_password',
+        is_expired=True
+    )
-    sage_intacct_credentials.is_expired = True
-    sage_intacct_credentials.save()
    invalidate_sage_intacct_credentials(workspace_id)
    assert not mocked_patch.called

    # Should call patch_integration_settings with the correct arguments if sage_intacct_credentials.is_expired is False
+    sage_intacct_credentials.delete()
+    sage_intacct_credentials = SageIntacctCredential.objects.create(
+        workspace_id=workspace_id,
+        si_user_id='test_user',
+        si_company_id='test_company', 
+        si_user_password='test_password',
+        is_expired=False
+    )
-    sage_intacct_credentials.is_expired = False
-    sage_intacct_credentials.save()

    invalidate_sage_intacct_credentials(workspace_id)

    args, kwargs = mocked_patch.call_args
    assert args[0] == workspace_id
-    assert kwargs['is_token_expired'] == True
+    assert kwargs['is_token_expired']

    # Verify the credentials were marked as expired
    sage_intacct_credentials.refresh_from_db()
-    assert sage_intacct_credentials.is_expired == True
+    assert sage_intacct_credentials.is_expired

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Ruff (0.11.9)

1531-1531: Avoid equality comparisons to True; use if kwargs['is_token_expired']: for truth checks

Replace with kwargs['is_token_expired']

(E712)


1535-1535: Avoid equality comparisons to True; use if sage_intacct_credentials.is_expired: for truth checks

Replace with sage_intacct_credentials.is_expired

(E712)

🤖 Prompt for AI Agents
In tests/test_sageintacct/test_utils.py around lines 1502 to 1535, fix the
logical error by re-fetching or recreating the sage_intacct_credentials object
after it is deleted before accessing it again to avoid AttributeError. Also,
improve code style by removing explicit '== True' comparisons in assertions on
lines 1531 and 1535, using direct boolean checks instead.

@github-actions github-actions bot added size/L Large PR and removed size/M Medium PR labels May 28, 2025
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: 3

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a0c146b and 6ffd1ae.

📒 Files selected for processing (1)
  • tests/test_workspaces/test_utils.py (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
tests/test_workspaces/test_utils.py (1)
apps/workspaces/utils.py (1)
  • send_email (6-24)
🪛 GitHub Actions: Continuous Integration
tests/test_workspaces/test_utils.py

[error] 8-8: flake8: D103 Missing docstring in public function


[error] 18-18: flake8: D103 Missing docstring in public function


[error] 20-43: flake8: W293 blank line contains whitespace (lines 20, 26, 29, 32, 35, 43)


[error] 45-45: flake8: W291 trailing whitespace and W292 no newline at end of file

Comment on lines 1 to 5
import pytest
from unittest.mock import patch, MagicMock
from django.conf import settings
from apps.workspaces.utils import send_email

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add missing import for proper test coverage.

The test is missing an import for the From class which is used in the send_email implementation. This could cause the test assertion on line 42 to fail.

Add the missing import:

 import pytest
 from unittest.mock import patch, MagicMock
 from django.conf import settings
+from sendgrid.helpers.mail import From
 from apps.workspaces.utils import send_email
📝 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
import pytest
from unittest.mock import patch, MagicMock
from django.conf import settings
from apps.workspaces.utils import send_email
import pytest
from unittest.mock import patch, MagicMock
from django.conf import settings
from sendgrid.helpers.mail import From
from apps.workspaces.utils import send_email
🤖 Prompt for AI Agents
In tests/test_workspaces/test_utils.py at the beginning of the file (lines 1 to
5), add an import statement for the From class used in the send_email
implementation. This will ensure the test has proper coverage and prevent
assertion failures related to the missing From class reference.

Comment on lines 7 to 15
@pytest.fixture
def mock_sendgrid():
with patch('apps.workspaces.utils.SendGridAPIClient') as mock_sg, \
patch('apps.workspaces.utils.Mail') as mock_mail:
mock_instance = MagicMock()
mock_sg.return_value = mock_instance
mock_mail_instance = MagicMock()
mock_mail.return_value = mock_mail_instance
yield mock_sg, mock_mail
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

Add docstring and improve mock setup.

The fixture is missing a docstring as indicated by the pipeline failure. Additionally, consider mocking the From class for complete test isolation.

Apply this diff to address the issues:

 @pytest.fixture
 def mock_sendgrid():
+    """
+    Fixture to mock SendGrid components for testing email functionality.
+    """
-    with patch('apps.workspaces.utils.SendGridAPIClient') as mock_sg, \
-         patch('apps.workspaces.utils.Mail') as mock_mail:
+    with patch('apps.workspaces.utils.SendGridAPIClient') as mock_sg, \
+         patch('apps.workspaces.utils.Mail') as mock_mail, \
+         patch('apps.workspaces.utils.From') as mock_from:
         mock_instance = MagicMock()
         mock_sg.return_value = mock_instance
         mock_mail_instance = MagicMock()
         mock_mail.return_value = mock_mail_instance
-        yield mock_sg, mock_mail
+        mock_from_instance = MagicMock()
+        mock_from.return_value = mock_from_instance
+        yield mock_sg, mock_mail, mock_from
📝 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
@pytest.fixture
def mock_sendgrid():
with patch('apps.workspaces.utils.SendGridAPIClient') as mock_sg, \
patch('apps.workspaces.utils.Mail') as mock_mail:
mock_instance = MagicMock()
mock_sg.return_value = mock_instance
mock_mail_instance = MagicMock()
mock_mail.return_value = mock_mail_instance
yield mock_sg, mock_mail
@pytest.fixture
def mock_sendgrid():
"""
Fixture to mock SendGrid components for testing email functionality.
"""
with patch('apps.workspaces.utils.SendGridAPIClient') as mock_sg, \
patch('apps.workspaces.utils.Mail') as mock_mail, \
patch('apps.workspaces.utils.From') as mock_from:
mock_instance = MagicMock()
mock_sg.return_value = mock_instance
mock_mail_instance = MagicMock()
mock_mail.return_value = mock_mail_instance
mock_from_instance = MagicMock()
mock_from.return_value = mock_from_instance
yield mock_sg, mock_mail, mock_from
🧰 Tools
🪛 GitHub Actions: Continuous Integration

[error] 8-8: flake8: D103 Missing docstring in public function

🤖 Prompt for AI Agents
In tests/test_workspaces/test_utils.py around lines 7 to 15, add a descriptive
docstring to the mock_sendgrid fixture explaining its purpose. Also, extend the
mock setup by patching the 'apps.workspaces.utils.From' class alongside
SendGridAPIClient and Mail to ensure complete test isolation. Update the fixture
to yield all three mocks accordingly.

Comment on lines 18 to 45
def test_send_email(mock_sendgrid):
mock_sg, mock_mail = mock_sendgrid

# Test data
recipient_email = ['[email protected]']
subject = 'Test Subject'
message = '<p>Test Message</p>'
sender_email = '[email protected]'

# Mock settings
settings.SENDGRID_API_KEY = 'test_api_key'

# Call the function
send_email(recipient_email, subject, message, sender_email)

# Verify SendGridAPIClient was initialized with correct API key
mock_sg.assert_called_once_with(api_key='test_api_key')

# Verify Mail was created with correct parameters
mock_mail.assert_called_once()
mail_args = mock_mail.call_args[1]
assert mail_args['to_emails'] == recipient_email
assert mail_args['subject'] == subject
assert mail_args['html_content'] == message
assert mail_args['from_email'].email == sender_email

# Verify mail was sent
mock_sg.return_value.send.assert_called_once_with(mock_mail.return_value)
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

Fix formatting issues and improve test assertions.

Multiple flake8 issues need to be addressed including missing docstring, whitespace issues, and missing newline at end of file. Also, update the test to handle the mocked From class.

Apply this diff to fix all formatting and logic issues:

 def test_send_email(mock_sendgrid):
+    """
+    Test that send_email correctly integrates with SendGrid API.
+    """
-    mock_sg, mock_mail = mock_sendgrid
-    
+    mock_sg, mock_mail, mock_from = mock_sendgrid
+
     # Test data
     recipient_email = ['[email protected]']
     subject = 'Test Subject'
     message = '<p>Test Message</p>'
     sender_email = '[email protected]'
-    
+
     # Mock settings
     settings.SENDGRID_API_KEY = 'test_api_key'
-    
+
     # Call the function
     send_email(recipient_email, subject, message, sender_email)
-    
+
     # Verify SendGridAPIClient was initialized with correct API key
     mock_sg.assert_called_once_with(api_key='test_api_key')
-    
+
+    # Verify From object was created with correct sender email
+    mock_from.assert_called_once_with(email=sender_email)
+
     # Verify Mail was created with correct parameters
     mock_mail.assert_called_once()
     mail_args = mock_mail.call_args[1]
     assert mail_args['to_emails'] == recipient_email
     assert mail_args['subject'] == subject
     assert mail_args['html_content'] == message
-    assert mail_args['from_email'].email == sender_email
-    
+    assert mail_args['from_email'] == mock_from.return_value
+
     # Verify mail was sent
-    mock_sg.return_value.send.assert_called_once_with(mock_mail.return_value) 
+    mock_sg.return_value.send.assert_called_once_with(mock_mail.return_value)
📝 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
def test_send_email(mock_sendgrid):
mock_sg, mock_mail = mock_sendgrid
# Test data
recipient_email = ['[email protected]']
subject = 'Test Subject'
message = '<p>Test Message</p>'
sender_email = '[email protected]'
# Mock settings
settings.SENDGRID_API_KEY = 'test_api_key'
# Call the function
send_email(recipient_email, subject, message, sender_email)
# Verify SendGridAPIClient was initialized with correct API key
mock_sg.assert_called_once_with(api_key='test_api_key')
# Verify Mail was created with correct parameters
mock_mail.assert_called_once()
mail_args = mock_mail.call_args[1]
assert mail_args['to_emails'] == recipient_email
assert mail_args['subject'] == subject
assert mail_args['html_content'] == message
assert mail_args['from_email'].email == sender_email
# Verify mail was sent
mock_sg.return_value.send.assert_called_once_with(mock_mail.return_value)
def test_send_email(mock_sendgrid):
"""
Test that send_email correctly integrates with SendGrid API.
"""
mock_sg, mock_mail, mock_from = mock_sendgrid
# Test data
recipient_email = ['[email protected]']
subject = 'Test Subject'
message = '<p>Test Message</p>'
sender_email = '[email protected]'
# Mock settings
settings.SENDGRID_API_KEY = 'test_api_key'
# Call the function
send_email(recipient_email, subject, message, sender_email)
# Verify SendGridAPIClient was initialized with correct API key
mock_sg.assert_called_once_with(api_key='test_api_key')
# Verify From object was created with correct sender email
mock_from.assert_called_once_with(email=sender_email)
# Verify Mail was created with correct parameters
mock_mail.assert_called_once()
mail_args = mock_mail.call_args[1]
assert mail_args['to_emails'] == recipient_email
assert mail_args['subject'] == subject
assert mail_args['html_content'] == message
assert mail_args['from_email'] == mock_from.return_value
# Verify mail was sent
mock_sg.return_value.send.assert_called_once_with(mock_mail.return_value)
# (Ensure there is a trailing newline at end of file)
🧰 Tools
🪛 GitHub Actions: Continuous Integration

[error] 18-18: flake8: D103 Missing docstring in public function


[error] 20-43: flake8: W293 blank line contains whitespace (lines 20, 26, 29, 32, 35, 43)


[error] 45-45: flake8: W291 trailing whitespace and W292 no newline at end of file

🤖 Prompt for AI Agents
In tests/test_workspaces/test_utils.py around lines 18 to 45, fix flake8
formatting issues by adding a proper docstring to the test function, correcting
any whitespace inconsistencies, and ensuring there is a newline at the end of
the file. Additionally, update the test assertions to correctly handle the
mocked From class by verifying the 'from_email' attribute appropriately,
ensuring the test logic aligns with the mock structure.

@ruuushhh ruuushhh temporarily deployed to CI Environment May 28, 2025 07:15 — with GitHub Actions Inactive
Copy link

Coverage

Coverage Report
FileStmtsMissCoverMissing
apps
   exceptions.py471177%27, 33, 39–44, 50, 56–61, 67, 73, 79–80
apps/fyle
   actions.py117794%212, 261, 317–321
   constants.py10100% 
   helpers.py2492690%127, 210–245, 290, 582, 589–592, 595–596
   models.py3362393%313–315, 319–321, 325–327, 331–333, 337–339, 343–345, 406, 512–545, 568, 585
   queue.py280100% 
   serializers.py410100% 
   signals.py35683%23, 56–57, 70–72
   tasks.py1976666%164–169, 172–177, 180–185, 211–221, 233–238, 257, 281–286, 289–291, 295, 348–395
   views.py1881592%96–103, 271, 356–357, 390–391, 416–420, 513–517
apps/internal
   actions.py180100% 
   views.py40198%37
apps/mappings
   constants.py10100% 
   exceptions.py66395%69–71
   helpers.py20575%45–55
   models.py700100% 
   serializers.py100100% 
   signals.py84792%85–87, 122, 169–174
   tasks.py1071982%112–126, 245, 248, 251, 256–263
   utils.py360100% 
   views.py51296%35–36
apps/mappings/imports
   schedules.py150100% 
apps/mappings/imports/modules
   base.py153299%122, 345
   categories.py64592%112–113, 123, 125, 157
   cost_centers.py55591%91–92, 103, 105, 139
   expense_custom_fields.py69396%60–65
   merchants.py260100% 
   projects.py61493%94–95, 111, 147
   tax_groups.py150100% 
apps/sage_intacct
   dependent_fields.py2644085%307, 343–346, 375–384, 386–387, 389–391, 393–394, 396–397, 401–403, 447, 467, 507–509, 555–557, 605–611, 615–616
   helpers.py540100% 
   models.py7893596%70–71, 221, 328, 335, 379, 400–417, 476, 538, 544, 579, 749, 1066–1069, 1236–1256, 1346, 1778
   queue.py1901990%77–80, 126, 142, 150, 179–182, 244–247, 309–312
   serializers.py310100% 
   tasks.py10049391%100–101, 127–130, 164, 200–201, 205–208, 210, 213, 263, 297–298, 443, 449, 457, 612–613, 633–634, 680–681, 785–786, 853–854, 957–958, 1014–1015, 1116–1117, 1173–1174, 1302–1303, 1369–1378, 1420–1444, 1563–1567, 1623–1629, 1715–1725, 1728, 1762–1764, 1798–1801, 1863, 1872
   utils.py7886891%152, 382–383, 388, 401–402, 410–411, 474–475, 549–553, 657, 897–920, 1333–1355, 1378–1388, 1616–1620, 1648–1652, 1680–1684, 1726–1730, 1783–1787, 1807, 1816, 1825, 1834–1835, 1846, 1869–1871, 2028
   views.py1181686%193–195, 200–201, 207–208, 242–245, 269, 289–292
apps/sage_intacct/errors
   errors.py20100% 
   helpers.py26196%73
apps/tasks
   models.py670100% 
   serializers.py60100% 
   views.py370100% 
apps/users
   helpers.py120100% 
   models.py530100% 
   views.py240100% 
apps/workspaces
   actions.py46296%42, 126
   models.py1090100% 
   permissions.py34974%33, 59–67
   serializers.py39197%53
   signals.py26196%70
   tasks.py110397%179, 209–210
   utils.py90100% 
   views.py215797%69–72, 105, 429–430
apps/workspaces/apis/advanced_settings
   serializers.py78396%247, 250, 253
   triggers.py100100% 
   views.py110100% 
apps/workspaces/apis/errors
   serializers.py200100% 
   views.py150100% 
apps/workspaces/apis/export_settings
   serializers.py98397%256, 259, 262
   triggers.py27293%33–34
   views.py110100% 
apps/workspaces/apis/import_settings
   serializers.py1401788%231–237, 242–248, 256, 274, 277, 305, 308–309, 317
   triggers.py47198%31
   views.py330100% 
workers/export
   actions.py11191%20
   worker.py51884%39–41, 44–45, 73–74, 107
TOTAL680554092% 

Tests Skipped Failures Errors Time
319 0 💤 0 ❌ 0 🔥 39.516s ⏱️

@ruuushhh ruuushhh requested a review from ashwin1111 May 29, 2025 06:32
@ruuushhh ruuushhh requested a review from ashwin1111 May 29, 2025 18:56
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

♻️ Duplicate comments (1)
apps/sage_intacct/tasks.py (1)

729-735: Improved exception handling structure.

Splitting the exception blocks improves code readability and maintainability.

Note: There's an empty line at line 730 that was previously flagged for removal.

🧹 Nitpick comments (1)
apps/internal/actions.py (1)

15-19: Good credential handling improvement with minor enhancement opportunity.

The changes correctly implement the new credential retrieval pattern and provide clear error handling. The try-except block ensures graceful handling when credentials are missing.

Consider improving exception chaining for better debugging:

    except SageIntacctCredential.DoesNotExist:
-        raise Exception('Sage Intacct credentials not found')
+        raise Exception('Sage Intacct credentials not found') from None

This follows the static analysis recommendation and makes it clear that the original exception context is intentionally omitted.

🧰 Tools
🪛 Ruff (0.11.9)

19-19: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7ce5d72 and 23d47c5.

📒 Files selected for processing (10)
  • apps/internal/actions.py (1 hunks)
  • apps/mappings/imports/modules/base.py (1 hunks)
  • apps/mappings/tasks.py (4 hunks)
  • apps/sage_intacct/helpers.py (2 hunks)
  • apps/sage_intacct/tasks.py (17 hunks)
  • apps/sage_intacct/views.py (5 hunks)
  • apps/workspaces/models.py (1 hunks)
  • apps/workspaces/tasks.py (1 hunks)
  • apps/workspaces/views.py (3 hunks)
  • tests/test_workspaces/test_utils.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • tests/test_workspaces/test_utils.py
  • apps/sage_intacct/helpers.py
  • apps/workspaces/models.py
  • apps/mappings/tasks.py
  • apps/sage_intacct/views.py
🧰 Additional context used
🧬 Code Graph Analysis (4)
apps/workspaces/tasks.py (1)
apps/workspaces/models.py (2)
  • SageIntacctCredential (166-185)
  • get_active_sage_intacct_credentials (184-185)
apps/workspaces/views.py (1)
apps/workspaces/models.py (2)
  • SageIntacctCredential (166-185)
  • get_active_sage_intacct_credentials (184-185)
apps/internal/actions.py (2)
apps/workspaces/models.py (2)
  • SageIntacctCredential (166-185)
  • get_active_sage_intacct_credentials (184-185)
apps/sage_intacct/utils.py (1)
  • SageIntacctConnector (76-2096)
apps/sage_intacct/tasks.py (3)
fyle_intacct_api/utils.py (1)
  • invalidate_sage_intacct_credentials (30-38)
apps/workspaces/models.py (2)
  • SageIntacctCredential (166-185)
  • get_active_sage_intacct_credentials (184-185)
apps/sage_intacct/utils.py (2)
  • SageIntacctConnector (76-2096)
  • search_and_create_vendors (2057-2096)
🪛 Ruff (0.11.9)
apps/internal/actions.py

19-19: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

apps/sage_intacct/tasks.py

1862-1863: SyntaxError: Expected ':', found newline

🪛 GitHub Actions: Continuous Integration
apps/sage_intacct/tasks.py

[error] 1862-1862: SyntaxError: expected ':' at line 1862, column 17.

🔇 Additional comments (13)
apps/workspaces/tasks.py (1)

160-160: LGTM! Credential retrieval pattern improved.

The replacement of direct ORM query with SageIntacctCredential.get_active_sage_intacct_credentials(workspace_id) correctly centralizes credential retrieval logic and ensures only active (non-expired) credentials are used. The existing exception handling appropriately logs and returns early when credentials are missing.

apps/workspaces/views.py (3)

282-282: LGTM! Consistent credential retrieval pattern.

The replacement with SageIntacctCredential.get_active_sage_intacct_credentials(workspace_id) ensures only active credentials are retrieved, aligning with the broader credential management improvements.


329-335: Excellent addition of explicit exception handling.

This explicit exception handling for SageIntacctCredential.DoesNotExist in the post method is a valuable improvement. It provides a clear, actionable error message and consistent error handling that was previously missing. The error message format matches the pattern used elsewhere in the codebase.


383-383: LGTM! Consistent with credential management refactoring.

The use of get_active_sage_intacct_credentials ensures only non-expired credentials are retrieved, maintaining consistency with the updated credential handling approach.

apps/mappings/imports/modules/base.py (1)

211-233: Excellent defensive programming approach.

The wrapping of credential retrieval and synchronization logic in a try-except block is well-implemented. The method gracefully handles missing credentials by logging an informational message and returning early, preventing unnecessary processing. The use of get_active_sage_intacct_credentials ensures only active credentials are used for synchronization operations.

The error handling pattern is consistent with other files in this refactoring effort and maintains the method's existing functionality when credentials are available.

apps/sage_intacct/tasks.py (8)

14-14: LGTM!

Good addition of the utility function import to support credential invalidation throughout the file.


278-283: Excellent improvement to credential handling.

The standardized credential retrieval with proper exception handling and graceful failure is a good improvement over direct ORM queries.


626-626: Good standardization of credential retrieval.

Consistent use of the new class method improves maintainability.


736-742: Excellent addition of token invalidation handling.

The specific handling for InvalidTokenError with credential invalidation improves the robustness of token management.


799-799: Good credential retrieval standardization.

Consistent use of the new class method.


904-909: Consistent exception handling improvement.

The split exception blocks follow the established pattern and improve code structure.


910-916: Consistent token invalidation handling.

Proper implementation following the established pattern for handling expired tokens.


970-970: Consistent implementation across all functions.

All these changes follow the established patterns for:

  • Standardized credential retrieval using the new class method
  • Proper exception handling with separated blocks for NoPrivilegeError and InvalidTokenError
  • Token invalidation when credentials expire

These improvements enhance code maintainability and robustness.

Also applies to: 1063-1075, 1130-1130, 1223-1235, 1367-1367, 1427-1427, 1497-1497, 1559-1559, 1625-1625, 1627-1633

@@ -810,7 +823,7 @@
expense_group, configuration
)

sage_intacct_credentials = SageIntacctCredential.objects.get(workspace_id=expense_group.workspace_id)
sage_intacct_credentials = SageIntacctCredential.get_active_sage_intacct_credentials(expense_group.workspace_id)
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Remove redundant credential retrieval.

Credentials are already retrieved at line 799 in the same function. This duplicate retrieval is unnecessary and should be removed.

-            sage_intacct_credentials = SageIntacctCredential.get_active_sage_intacct_credentials(expense_group.workspace_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
sage_intacct_credentials = SageIntacctCredential.get_active_sage_intacct_credentials(expense_group.workspace_id)
🤖 Prompt for AI Agents
In apps/sage_intacct/tasks.py at line 826, the code redundantly retrieves Sage
Intacct credentials which were already fetched at line 799 in the same function.
Remove the line at 826 that calls
SageIntacctCredential.get_active_sage_intacct_credentials to avoid duplicate
retrieval and rely on the credentials obtained earlier.

Copy link

Coverage

Coverage Report
FileStmtsMissCoverMissing
apps
   exceptions.py471177%27, 33, 39–44, 50, 56–61, 67, 73, 79–80
apps/fyle
   actions.py117794%212, 261, 317–321
   constants.py10100% 
   helpers.py2492690%127, 210–245, 290, 582, 589–592, 595–596
   models.py3362393%313–315, 319–321, 325–327, 331–333, 337–339, 343–345, 406, 512–545, 568, 585
   queue.py280100% 
   serializers.py410100% 
   signals.py35683%23, 56–57, 70–72
   tasks.py1976666%164–169, 172–177, 180–185, 211–221, 233–238, 257, 281–286, 289–291, 295, 348–395
   views.py1881592%96–103, 271, 356–357, 390–391, 416–420, 513–517
apps/internal
   actions.py21290%18–19
   views.py40198%37
apps/mappings
   constants.py10100% 
   exceptions.py66395%69–71
   helpers.py20575%45–55
   models.py700100% 
   serializers.py100100% 
   signals.py84792%85–87, 122, 169–174
   tasks.py1112280%112–126, 227–229, 249, 252, 255, 260–267
   utils.py360100% 
   views.py51296%35–36
apps/mappings/imports
   schedules.py150100% 
apps/mappings/imports/modules
   base.py157597%122, 231–233, 349
   categories.py64592%112–113, 123, 125, 157
   cost_centers.py55591%91–92, 103, 105, 139
   expense_custom_fields.py69396%60–65
   merchants.py260100% 
   projects.py61493%94–95, 111, 147
   tax_groups.py150100% 
apps/sage_intacct
   dependent_fields.py2644085%307, 343–346, 375–384, 386–387, 389–391, 393–394, 396–397, 401–403, 447, 467, 507–509, 555–557, 605–611, 615–616
   helpers.py57296%71–72
   models.py7893596%70–71, 221, 328, 335, 379, 400–417, 476, 538, 544, 579, 749, 1066–1069, 1236–1256, 1346, 1778
   queue.py1901990%77–80, 126, 142, 150, 179–182, 244–247, 309–312
   serializers.py310100% 
   tasks.py101212088%100–101, 127–130, 164, 200–201, 205–208, 210, 213, 263, 281–283, 301–302, 371–373, 447, 453, 461, 616–617, 637–638, 684–685, 729–731, 741–744, 786–787, 854–855, 902–905, 958–959, 1015–1016, 1061–1064, 1117–1118, 1174–1175, 1221–1224, 1234–1237, 1303–1304, 1370–1379, 1421–1445, 1564–1568, 1624–1630, 1716–1726, 1729, 1763–1765, 1799–1802, 1864–1868, 1877
   utils.py7886891%152, 382–383, 388, 401–402, 410–411, 474–475, 549–553, 657, 897–920, 1333–1355, 1378–1388, 1616–1620, 1648–1652, 1680–1684, 1726–1730, 1783–1787, 1807, 1816, 1825, 1834–1835, 1846, 1869–1871, 2028
   views.py1181686%193–195, 200–201, 207–208, 242–245, 269, 289–292
apps/sage_intacct/errors
   errors.py20100% 
   helpers.py26196%73
apps/tasks
   models.py670100% 
   serializers.py60100% 
   views.py370100% 
apps/users
   helpers.py120100% 
   models.py530100% 
   views.py240100% 
apps/workspaces
   actions.py46296%42, 126
   models.py1120100% 
   permissions.py34974%33, 59–67
   serializers.py39197%53
   signals.py26196%70
   tasks.py110397%179, 209–210
   utils.py90100% 
   views.py2172091%69–72, 105, 263–268, 291–309, 337, 344, 351, 358, 436–437
apps/workspaces/apis/advanced_settings
   serializers.py78396%247, 250, 253
   triggers.py100100% 
   views.py110100% 
apps/workspaces/apis/errors
   serializers.py200100% 
   views.py150100% 
apps/workspaces/apis/export_settings
   serializers.py98397%256, 259, 262
   triggers.py27293%33–34
   views.py110100% 
apps/workspaces/apis/import_settings
   serializers.py1401788%231–237, 242–248, 256, 274, 277, 305, 308–309, 317
   triggers.py47198%31
   views.py330100% 
workers/export
   actions.py11191%20
   worker.py51884%39–41, 44–45, 73–74, 107
TOTAL683259091% 

Tests Skipped Failures Errors Time
319 0 💤 4 ❌ 0 🔥 42.367s ⏱️

Copy link

Coverage

Coverage Report
FileStmtsMissCoverMissing
apps
   exceptions.py471177%27, 33, 39–44, 50, 56–61, 67, 73, 79–80
apps/fyle
   actions.py117794%212, 261, 317–321
   constants.py10100% 
   helpers.py2492690%127, 210–245, 290, 582, 589–592, 595–596
   models.py3362393%313–315, 319–321, 325–327, 331–333, 337–339, 343–345, 406, 512–545, 568, 585
   queue.py280100% 
   serializers.py410100% 
   signals.py35683%23, 56–57, 70–72
   tasks.py1976666%164–169, 172–177, 180–185, 211–221, 233–238, 257, 281–286, 289–291, 295, 348–395
   views.py1881592%96–103, 271, 356–357, 390–391, 416–420, 513–517
apps/internal
   actions.py21290%18–19
   views.py40198%37
apps/mappings
   constants.py10100% 
   exceptions.py66395%69–71
   helpers.py20575%45–55
   models.py700100% 
   serializers.py100100% 
   signals.py84792%85–87, 122, 169–174
   tasks.py1112280%112–126, 227–229, 249, 252, 255, 260–267
   utils.py360100% 
   views.py51296%35–36
apps/mappings/imports
   schedules.py150100% 
apps/mappings/imports/modules
   base.py157597%122, 231–233, 349
   categories.py64592%112–113, 123, 125, 157
   cost_centers.py55591%91–92, 103, 105, 139
   expense_custom_fields.py69396%60–65
   merchants.py260100% 
   projects.py61493%94–95, 111, 147
   tax_groups.py150100% 
apps/sage_intacct
   dependent_fields.py2644085%307, 343–346, 375–384, 386–387, 389–391, 393–394, 396–397, 401–403, 447, 467, 507–509, 555–557, 605–611, 615–616
   helpers.py57296%71–72
   models.py7893596%70–71, 221, 328, 335, 379, 400–417, 476, 538, 544, 579, 749, 1066–1069, 1236–1256, 1346, 1778
   queue.py1901990%77–80, 126, 142, 150, 179–182, 244–247, 309–312
   serializers.py310100% 
   tasks.py101210590%100–101, 127–130, 164, 200–201, 205–208, 210, 213, 263, 281–283, 301–302, 447, 453, 461, 616–617, 637–638, 684–685, 786–787, 854–855, 902–905, 958–959, 1015–1016, 1061–1064, 1117–1118, 1174–1175, 1303–1304, 1370–1379, 1421–1445, 1564–1568, 1624–1630, 1716–1726, 1729, 1763–1765, 1799–1802, 1864–1868, 1877
   utils.py7886891%152, 382–383, 388, 401–402, 410–411, 474–475, 549–553, 657, 897–920, 1333–1355, 1378–1388, 1616–1620, 1648–1652, 1680–1684, 1726–1730, 1783–1787, 1807, 1816, 1825, 1834–1835, 1846, 1869–1871, 2028
   views.py1181686%193–195, 200–201, 207–208, 242–245, 269, 289–292
apps/sage_intacct/errors
   errors.py20100% 
   helpers.py26196%73
apps/tasks
   models.py670100% 
   serializers.py60100% 
   views.py370100% 
apps/users
   helpers.py120100% 
   models.py530100% 
   views.py240100% 
apps/workspaces
   actions.py46296%42, 126
   models.py1120100% 
   permissions.py34974%33, 59–67
   serializers.py39197%53
   signals.py26196%70
   tasks.py110397%179, 209–210
   utils.py90100% 
   views.py217896%69–72, 105, 330, 436–437
apps/workspaces/apis/advanced_settings
   serializers.py78396%247, 250, 253
   triggers.py100100% 
   views.py110100% 
apps/workspaces/apis/errors
   serializers.py200100% 
   views.py150100% 
apps/workspaces/apis/export_settings
   serializers.py98397%256, 259, 262
   triggers.py27293%33–34
   views.py110100% 
apps/workspaces/apis/import_settings
   serializers.py1401788%231–237, 242–248, 256, 274, 277, 305, 308–309, 317
   triggers.py47198%31
   views.py330100% 
workers/export
   actions.py11191%20
   worker.py51884%39–41, 44–45, 73–74, 107
TOTAL683256392% 

Tests Skipped Failures Errors Time
319 0 💤 0 ❌ 0 🔥 40.720s ⏱️

@ruuushhh ruuushhh temporarily deployed to CI Environment May 29, 2025 20:14 — with GitHub Actions Inactive
@github-actions github-actions bot added size/XL Extra Large PR and removed size/L Large PR labels May 29, 2025
Copy link

Coverage

Coverage Report
FileStmtsMissCoverMissing
apps
   exceptions.py471177%27, 33, 39–44, 50, 56–61, 67, 73, 79–80
apps/fyle
   actions.py117794%212, 261, 317–321
   constants.py10100% 
   helpers.py2491793%127, 229–245, 290, 582, 589–592, 595–596
   models.py3362393%313–315, 319–321, 325–327, 331–333, 337–339, 343–345, 406, 512–545, 568, 585
   queue.py280100% 
   serializers.py410100% 
   signals.py35683%23, 56–57, 70–72
   tasks.py1976666%164–169, 172–177, 180–185, 211–221, 233–238, 257, 281–286, 289–291, 295, 348–395
   views.py1881592%96–103, 271, 356–357, 390–391, 416–420, 513–517
apps/internal
   actions.py21290%18–19
   views.py40198%37
apps/mappings
   constants.py10100% 
   exceptions.py66395%69–71
   helpers.py200100% 
   models.py700100% 
   serializers.py100100% 
   signals.py84792%85–87, 122, 169–174
   tasks.py1112280%112–126, 227–229, 249, 252, 255, 260–267
   utils.py360100% 
   views.py51296%35–36
apps/mappings/imports
   schedules.py150100% 
apps/mappings/imports/modules
   base.py157497%122, 231–233
   categories.py64592%112–113, 123, 125, 157
   cost_centers.py55591%91–92, 103, 105, 139
   expense_custom_fields.py69396%60–65
   merchants.py260100% 
   projects.py61493%94–95, 111, 147
   tax_groups.py150100% 
apps/sage_intacct
   dependent_fields.py2644085%307, 343–346, 375–384, 386–387, 389–391, 393–394, 396–397, 401–403, 447, 467, 507–509, 555–557, 605–611, 615–616
   helpers.py57296%71–72
   models.py7893596%70–71, 221, 328, 335, 379, 400–417, 476, 538, 544, 579, 749, 1066–1069, 1236–1256, 1346, 1778
   queue.py1901990%77–80, 126, 142, 150, 179–182, 244–247, 309–312
   serializers.py310100% 
   tasks.py101210590%100–101, 127–130, 164, 200–201, 205–208, 210, 213, 263, 281–283, 301–302, 447, 453, 461, 616–617, 637–638, 684–685, 786–787, 854–855, 902–905, 958–959, 1015–1016, 1061–1064, 1117–1118, 1174–1175, 1303–1304, 1370–1379, 1421–1445, 1564–1568, 1624–1630, 1716–1726, 1729, 1763–1765, 1799–1802, 1864–1868, 1877
   utils.py7886891%152, 382–383, 388, 401–402, 410–411, 474–475, 549–553, 657, 897–920, 1333–1355, 1378–1388, 1616–1620, 1648–1652, 1680–1684, 1726–1730, 1783–1787, 1807, 1816, 1825, 1834–1835, 1846, 1869–1871, 2028
   views.py1181686%193–195, 200–201, 207–208, 242–245, 269, 289–292
apps/sage_intacct/errors
   errors.py20100% 
   helpers.py26196%73
apps/tasks
   models.py670100% 
   serializers.py60100% 
   views.py370100% 
apps/users
   helpers.py120100% 
   models.py530100% 
   views.py240100% 
apps/workspaces
   actions.py46296%42, 126
   models.py1120100% 
   permissions.py34974%33, 59–67
   serializers.py39197%53
   signals.py260100% 
   tasks.py110397%179, 209–210
   utils.py90100% 
   views.py217896%69–72, 105, 330, 436–437
apps/workspaces/apis/advanced_settings
   serializers.py78396%247, 250, 253
   triggers.py100100% 
   views.py110100% 
apps/workspaces/apis/errors
   serializers.py200100% 
   views.py150100% 
apps/workspaces/apis/export_settings
   serializers.py98397%256, 259, 262
   triggers.py27293%33–34
   views.py110100% 
apps/workspaces/apis/import_settings
   serializers.py1401788%231–237, 242–248, 256, 274, 277, 305, 308–309, 317
   triggers.py47198%31
   views.py330100% 
workers/export
   actions.py110100% 
   worker.py51884%39–41, 44–45, 73–74, 107
TOTAL683254692% 

Tests Skipped Failures Errors Time
340 0 💤 0 ❌ 0 🔥 43.311s ⏱️

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: 0

♻️ Duplicate comments (3)
tests/test_workspaces/test_utils.py (3)

1-6: Missing import for the From class used in send_email function.

The test is missing an import for the From class from sendgrid.helpers.mail which is used in the send_email implementation. This will cause the test assertion on line 50 to fail because the from_email attribute references an unmocked From instance.

Add the missing import:

 import pytest
 from unittest.mock import patch, MagicMock
 from django.conf import settings
+from sendgrid.helpers.mail import From
 from apps.workspaces.utils import send_email
 from typing import Generator
 from apps.workspaces.serializers import ConfigurationSerializer

9-20: Mock the From class for complete test isolation.

The fixture is missing a mock for the From class which is instantiated in the send_email function. Without mocking this class, the test assertion on line 50 will not work as expected.

Apply this diff to add the missing From class mock:

 @pytest.fixture
 def mock_sendgrid() -> Generator[tuple[MagicMock, MagicMock], None, None]:
     """
     Mock SendGridAPIClient and Mail
     """
     with patch('apps.workspaces.utils.SendGridAPIClient') as mock_sg, \
-         patch('apps.workspaces.utils.Mail') as mock_mail:
+         patch('apps.workspaces.utils.Mail') as mock_mail, \
+         patch('apps.workspaces.utils.From') as mock_from:
         mock_instance = MagicMock()
         mock_sg.return_value = mock_instance
         mock_mail_instance = MagicMock()
         mock_mail.return_value = mock_mail_instance
-        yield mock_sg, mock_mail
+        mock_from_instance = MagicMock()
+        mock_from.return_value = mock_from_instance
+        yield mock_sg, mock_mail, mock_from

Also update the type annotation:

-def mock_sendgrid() -> Generator[tuple[MagicMock, MagicMock], None, None]:
+def mock_sendgrid() -> Generator[tuple[MagicMock, MagicMock, MagicMock], None, None]:

23-53: Update test to handle mocked From class correctly.

The test needs to be updated to unpack the additional mock from the fixture and verify the From class interaction properly.

Apply this diff to fix the test logic:

 def test_send_email(mock_sendgrid) -> None:
     """
     Test send_email
     """
-    mock_sg, mock_mail = mock_sendgrid
+    mock_sg, mock_mail, mock_from = mock_sendgrid

     # Test data
     recipient_email = ['[email protected]']
     subject = 'Test Subject'
     message = '<p>Test Message</p>'
     sender_email = '[email protected]'

     # Mock settings
     settings.SENDGRID_API_KEY = 'test_api_key'

     # Call the function
     send_email(recipient_email, subject, message, sender_email)

     # Verify SendGridAPIClient was initialized with correct API key
     mock_sg.assert_called_once_with(api_key='test_api_key')

+    # Verify From object was created with correct sender email
+    mock_from.assert_called_once_with(email=sender_email)
+
     # Verify Mail was created with correct parameters
     mock_mail.assert_called_once()
     mail_args = mock_mail.call_args[1]
     assert mail_args['to_emails'] == recipient_email
     assert mail_args['subject'] == subject
     assert mail_args['html_content'] == message
-    assert mail_args['from_email'].email == sender_email
+    assert mail_args['from_email'] == mock_from.return_value

     # Verify mail was sent
     mock_sg.return_value.send.assert_called_once_with(mock_mail.return_value)
🧹 Nitpick comments (7)
tests/test_workspaces/test_signal.py (1)

18-18: Fix typo in function name.

There's a typo in the function name: "configration" should be "configuration".

-def test_run_post_configration_triggers_auto_map_employees_update(db):
+def test_run_post_configuration_triggers_auto_map_employees_update(db):
tests/test_mappings/test_imports/test_modules/test_base.py (1)

500-521: Remove unused dummy classes

These dummy classes DummyError and DummyImportLog are defined but never used in any of the test functions. This is dead code that should be removed to maintain code cleanliness.

-class DummyError:
-    """
-    Dummy Error class
-    """
-    def __init__(self, id):
-        self.id = id
-
-
-class DummyImportLog:
-    """
-    Dummy Import Log class
-    """
-    def __init__(self):
-        self.status = 'IN_PROGRESS'
-        self.last_successful_run_at = None
-        self.error_log = []
-        self.total_batches_count = 0
-        self.processed_batches_count = 0
-
-    def save(self):
-        self.saved = True
tests/test_workspaces/test_utils.py (1)

56-63: Consider adding more comprehensive test coverage for ConfigurationSerializer.

The current test only validates that data is returned unchanged when partial=True. Consider adding tests for various validation scenarios.

Consider adding additional test cases:

def test_configuration_serializer_full_validate():
    """
    Test ConfigurationSerializer.validate with partial=False
    """
    data = {'required_field': 'value'}
    serializer = ConfigurationSerializer(partial=False)
    result = serializer.validate(data)
    assert result == data

def test_configuration_serializer_empty_data():
    """
    Test ConfigurationSerializer.validate with empty data
    """
    data = {}
    serializer = ConfigurationSerializer(partial=True)
    result = serializer.validate(data)
    assert result == data
tests/test_mappings/test_helpers.py (1)

81-88: LGTM! Correct test for recent sync prevention.

The test correctly verifies that sync is prevented when the last successful run was very recent (current time), which should return False as the last run is within the 30-minute threshold.

Note: Line 88 appears to be missing the ~ annotation marker, but this doesn't affect the test functionality.

tests/test_fyle/test_helpers_api.py (3)

67-73: Improve test isolation by resetting all mocks.

While the test resets mock_sync and mock_workspace.save, it doesn't reset mock_get. For better test isolation, consider resetting all relevant mocks between test scenarios.

 # With source_synced_at, but > 0 days
 mock_sync.reset_mock()
 mock_workspace.save.reset_mock()
+mock_get.reset_mock()
 mock_workspace.source_synced_at = datetime.now(timezone.utc) - timedelta(days=1)

99-103: Reset mocks between test scenarios for clearer assertions.

The second test scenario reuses the same mock objects without resetting them, making the assertions less clear about what calls were made in each scenario.

 # No dependent_field_settings
+mock_platform_instance.import_fyle_dimensions.reset_mock()
+mock_update.reset_mock()
 mock_filter.return_value.first.return_value = None
 helpers.sync_dimensions(1)
 mock_platform_instance.import_fyle_dimensions.assert_called()
 mock_update.assert_called()

106-136: Enhance test coverage for conditional branches in handle_refresh_dimensions.

The test sets up configuration flags like import_vendors_as_merchants=True and import_categories=True but doesn't verify that the corresponding chain.append() calls are made for these specific scenarios. Consider adding assertions to verify the expected number of chain operations.

 helpers.handle_refresh_dimensions(1)
+# Verify that chain.append was called the expected number of times
+# (1 for mapping, 1 for vendors, 1 for categories = 3 total)
+assert mock_chain_instance.append.call_count == 3
 mock_chain_instance.run.assert_called_once()
 mock_sync.assert_called_once_with(1)
 mock_workspace.save.assert_called()

Additionally, consider testing edge cases where configuration is None or different expense object types to ensure full branch coverage.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3b6c16e and d72b25b.

📒 Files selected for processing (7)
  • tests/test_fyle/test_helpers_api.py (1 hunks)
  • tests/test_mappings/test_helpers.py (1 hunks)
  • tests/test_mappings/test_imports/test_modules/test_base.py (2 hunks)
  • tests/test_sageintacct/test_models.py (2 hunks)
  • tests/test_workspaces/test_signal.py (1 hunks)
  • tests/test_workspaces/test_utils.py (1 hunks)
  • tests/workers/export/test_worker.py (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (5)
tests/test_workspaces/test_utils.py (1)
apps/workspaces/utils.py (1)
  • send_email (6-24)
tests/test_sageintacct/test_models.py (1)
apps/workspaces/models.py (3)
  • Workspace (48-70)
  • SageIntacctCredential (166-190)
  • get_active_sage_intacct_credentials (184-190)
tests/test_fyle/test_helpers_api.py (1)
apps/fyle/helpers.py (4)
  • get_request (69-104)
  • patch_request (107-129)
  • add_expense_id_to_expense_group_settings (171-182)
  • handle_refresh_dimensions (248-308)
tests/test_mappings/test_helpers.py (1)
apps/mappings/helpers.py (3)
  • schedule_or_delete_auto_mapping_tasks (13-23)
  • prepend_code_to_name (26-36)
  • is_project_sync_allowed (39-55)
tests/test_mappings/test_imports/test_modules/test_base.py (2)
apps/mappings/imports/modules/base.py (5)
  • Base (28-379)
  • resolve_expense_attribute_errors (130-144)
  • create_ccc_category_mappings (146-157)
  • update_import_log_post_import (337-351)
  • check_import_log_and_start_import (353-379)
apps/mappings/tasks.py (1)
  • resolve_expense_attribute_errors (55-77)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: pytest
🔇 Additional comments (19)
tests/test_workspaces/test_signal.py (1)

18-31: LGTM! Test correctly validates signal behavior.

The test properly validates that the post-save signal updates auto_map_employees from False to 'NAME' under the specified conditions. The test setup with specific configuration values and the assertion logic correctly verifies the expected signal-triggered behavior.

tests/workers/export/test_worker.py (2)

7-7: LGTM!

The import statement is correctly placed and necessary for the new test function.


111-118: LGTM! Well-structured test with clear intent.

The test effectively verifies that handle_exports unpacks the input dictionary and passes it as keyword arguments to import_and_export_expenses. The test follows good practices:

  • Clear, descriptive test name
  • Proper use of mocking to isolate the unit under test
  • Focused assertion that validates the expected behavior
  • Consistent with the existing test patterns in the file
tests/test_sageintacct/test_models.py (2)

15-15: LGTM: Import statement correctly added.

The import for SageIntacctCredential is appropriately added to support the new test function. This aligns with the enhanced credential handling introduced in this PR.


1312-1320: LGTM: Well-structured test for the new credential method.

The test correctly validates the get_active_sage_intacct_credentials static method by:

  • Mocking the ORM get method appropriately
  • Verifying the method calls the ORM with correct filters (workspace_id=123, is_expired=False)
  • Asserting the expected return value

This test ensures that only active (non-expired) credentials are retrieved, which is essential for the enhanced credential management introduced in this PR.

tests/test_mappings/test_imports/test_modules/test_base.py (6)

22-24: LGTM: Clean import additions

The new imports for patch, MagicMock, and Base are appropriate for the test functions being added and follow proper conventions.


523-540: Well-structured test for error resolution logic

This test properly validates the conditional logic in resolve_expense_attribute_errors. It correctly tests both the case where no errored attributes exist and where errored attributes exist but no mapped attributes are found.


542-556: Good test for conditional category mapping logic

The test correctly validates that bulk_create_ccc_category_mappings is not called when the configuration conditions are not met. The mock setup appropriately simulates the configuration object.


558-570: Effective test for batch processing logic

This test properly validates the update_import_log_post_import method behavior when is_last_batch is False, ensuring the processed batch count increments and the log is saved.


572-586: Good coverage of import progress prevention

This test effectively validates that the import process doesn't start when an import log is already in progress and not newly created, which is important for preventing concurrent imports.


588-602: Proper test for time-based import prevention

This test correctly validates that the import process doesn't start when the sync_after timestamp is in the future, which is crucial for scheduled import timing.

tests/test_mappings/test_helpers.py (5)

1-8: LGTM! Clean imports and test setup.

The imports are appropriate for the test functionality, including the necessary mocking utilities and the functions being tested.


10-17: LGTM! Simple and effective test helper class.

The DummyConfig class provides a clean way to mock configuration objects with the required attributes for testing.


19-42: LGTM! Comprehensive test coverage for conditional logic.

The test correctly verifies the conditional behavior of schedule_or_delete_auto_mapping_tasks:

  • When auto_map_employees=True: charge card mapping is skipped
  • When auto_map_employees=False: all three functions are called
  • Proper mock reset between test scenarios

The test logic aligns perfectly with the implementation in apps/mappings/helpers.py.


44-54: LGTM! Thorough coverage of all logical branches.

The test covers all scenarios for the prepend_code_to_name function:

  • Code prepending when flag is True and code is provided
  • No prepending when flag is False
  • No prepending when code is None (edge case)

All assertions match the expected behavior from the implementation.


56-79: LGTM! Good coverage of sync allowance conditions.

These tests properly verify the conditions where project sync should be allowed:

  • When import_log is None
  • When last_successful_run_at is None
  • When the last run was over 30 minutes ago (1 hour in the test)

The test logic correctly reflects the implementation's time-based logic.

tests/test_fyle/test_helpers_api.py (3)

9-20: LGTM! Error handling test is correctly implemented.

The test properly mocks a 400 status response and verifies that the get_request function raises an exception with the expected error message.


24-35: LGTM! Error handling test is correctly implemented.

The test properly mocks a 400 status response and verifies that the patch_request function raises an exception with the expected error message.


39-49: LGTM! Test correctly verifies the expense ID addition logic.

The test properly mocks the ExpenseGroupSettings object and verifies that expense_id is added to the corporate credit card expense group fields and the save method is called.

Copy link

github-actions bot commented Jun 2, 2025

Coverage

Coverage Report
FileStmtsMissCoverMissing
apps
   exceptions.py471177%27, 33, 39–44, 50, 56–61, 67, 73, 79–80
apps/fyle
   actions.py117794%212, 261, 317–321
   constants.py10100% 
   helpers.py2581893%127, 229–245, 290, 504, 586, 593–596, 599–600
   models.py3362393%313–315, 319–321, 325–327, 331–333, 337–339, 343–345, 406, 512–545, 568, 585
   queue.py280100% 
   serializers.py410100% 
   signals.py50688%28, 61–62, 75–77
   tasks.py1946566%160–161, 164–165, 168–169, 191–201, 213–218, 239, 268–287, 339–386
   views.py1881592%96–103, 271, 356–357, 390–391, 416–420, 513–517
apps/internal
   actions.py21290%18–19
   views.py40198%37
apps/mappings
   constants.py10100% 
   exceptions.py66395%69–71
   helpers.py200100% 
   models.py700100% 
   serializers.py100100% 
   signals.py84792%85–87, 122, 169–174
   tasks.py1112280%112–126, 227–229, 249, 252, 255, 260–267
   utils.py360100% 
   views.py51296%35–36
apps/mappings/imports
   schedules.py150100% 
apps/mappings/imports/modules
   base.py157497%122, 231–233
   categories.py64592%112–113, 123, 125, 157
   cost_centers.py55591%91–92, 103, 105, 139
   expense_custom_fields.py69396%60–65
   merchants.py260100% 
   projects.py61493%94–95, 111, 147
   tax_groups.py150100% 
apps/sage_intacct
   dependent_fields.py2644085%307, 343–346, 375–384, 386–387, 389–391, 393–394, 396–397, 401–403, 447, 467, 507–509, 555–557, 605–611, 615–616
   helpers.py57296%71–72
   models.py7893596%70–71, 221, 328, 335, 379, 400–417, 476, 538, 544, 579, 749, 1066–1069, 1236–1256, 1346, 1778
   queue.py1901990%77–80, 126, 142, 150, 179–182, 244–247, 309–312
   serializers.py310100% 
   tasks.py101210590%100–101, 127–130, 164, 200–201, 205–208, 210, 213, 263, 281–283, 301–302, 447, 453, 461, 616–617, 637–638, 684–685, 786–787, 854–855, 902–905, 958–959, 1015–1016, 1061–1064, 1117–1118, 1174–1175, 1303–1304, 1370–1379, 1421–1445, 1564–1568, 1624–1630, 1716–1726, 1729, 1763–1765, 1799–1802, 1864–1868, 1877
   utils.py7886891%152, 382–383, 388, 401–402, 410–411, 474–475, 549–553, 657, 897–920, 1333–1355, 1378–1388, 1616–1620, 1648–1652, 1680–1684, 1726–1730, 1783–1787, 1807, 1816, 1825, 1834–1835, 1846, 1869–1871, 2028
   views.py1191687%195–197, 202–203, 209–210, 244–247, 271, 291–294
apps/sage_intacct/errors
   errors.py20100% 
   helpers.py26196%73
apps/tasks
   models.py670100% 
   serializers.py60100% 
   views.py370100% 
apps/users
   helpers.py120100% 
   models.py530100% 
   views.py240100% 
apps/workspaces
   actions.py46296%42, 126
   models.py1130100% 
   permissions.py34974%33, 59–67
   serializers.py39197%53
   signals.py260100% 
   tasks.py116992%87–91, 112, 185, 215–216
   utils.py90100% 
   views.py216896%68–71, 104, 329, 435–436
apps/workspaces/apis/advanced_settings
   serializers.py78396%248, 251, 254
   triggers.py100100% 
   views.py110100% 
apps/workspaces/apis/errors
   serializers.py200100% 
   views.py150100% 
apps/workspaces/apis/export_settings
   serializers.py98397%256, 259, 262
   triggers.py27293%33–34
   views.py110100% 
apps/workspaces/apis/import_settings
   serializers.py1401788%231–237, 242–248, 256, 274, 277, 305, 308–309, 317
   triggers.py47198%31
   views.py330100% 
workers/export
   actions.py110100% 
   worker.py46589%37–38, 66–67, 100
TOTAL685554992% 

Tests Skipped Failures Errors Time
345 0 💤 0 ❌ 0 🔥 40.919s ⏱️

@ruuushhh ruuushhh temporarily deployed to CI Environment June 2, 2025 11:32 — with GitHub Actions Inactive
@github-actions github-actions bot added size/L Large PR and removed size/XL Extra Large PR labels Jun 2, 2025
@ruuushhh ruuushhh requested a review from ashwin1111 June 2, 2025 11:32
Copy link

github-actions bot commented Jun 2, 2025

Coverage

Coverage Report
FileStmtsMissCoverMissing
apps
   exceptions.py471177%27, 33, 39–44, 50, 56–61, 67, 73, 79–80
apps/fyle
   actions.py117794%212, 261, 317–321
   constants.py10100% 
   helpers.py2581893%127, 229–245, 290, 504, 586, 593–596, 599–600
   models.py3362393%313–315, 319–321, 325–327, 331–333, 337–339, 343–345, 406, 512–545, 568, 585
   queue.py280100% 
   serializers.py410100% 
   signals.py50688%28, 61–62, 75–77
   tasks.py1946566%160–161, 164–165, 168–169, 191–201, 213–218, 239, 268–287, 339–386
   views.py1881592%96–103, 271, 356–357, 390–391, 416–420, 513–517
apps/internal
   actions.py21290%18–19
   views.py40198%37
apps/mappings
   constants.py10100% 
   exceptions.py66395%69–71
   helpers.py200100% 
   models.py700100% 
   serializers.py100100% 
   signals.py84792%85–87, 122, 169–174
   tasks.py1071982%112–126, 245, 248, 251, 256–263
   utils.py360100% 
   views.py51296%35–36
apps/mappings/imports
   schedules.py150100% 
apps/mappings/imports/modules
   base.py153199%122
   categories.py64592%112–113, 123, 125, 157
   cost_centers.py55591%91–92, 103, 105, 139
   expense_custom_fields.py69396%60–65
   merchants.py260100% 
   projects.py61493%94–95, 111, 147
   tax_groups.py150100% 
apps/sage_intacct
   dependent_fields.py2644085%307, 343–346, 375–384, 386–387, 389–391, 393–394, 396–397, 401–403, 447, 467, 507–509, 555–557, 605–611, 615–616
   helpers.py57296%71–72
   models.py7893596%70–71, 221, 328, 335, 379, 400–417, 476, 538, 544, 579, 749, 1066–1069, 1236–1256, 1346, 1778
   queue.py1901990%77–80, 126, 142, 150, 179–182, 244–247, 309–312
   serializers.py310100% 
   tasks.py100810290%100–101, 127–130, 164, 200–201, 205–208, 210, 213, 263, 297–298, 443, 449, 457, 612–613, 633–634, 680–681, 782–783, 850–851, 898–901, 954–955, 1011–1012, 1057–1060, 1113–1114, 1170–1171, 1299–1300, 1366–1375, 1417–1441, 1560–1564, 1620–1626, 1712–1722, 1725, 1759–1761, 1795–1798, 1860–1864, 1873
   utils.py7886891%152, 382–383, 388, 401–402, 410–411, 474–475, 549–553, 657, 897–920, 1333–1355, 1378–1388, 1616–1620, 1648–1652, 1680–1684, 1726–1730, 1783–1787, 1807, 1816, 1825, 1834–1835, 1846, 1869–1871, 2028
   views.py1191687%195–197, 202–203, 209–210, 244–247, 271, 291–294
apps/sage_intacct/errors
   errors.py20100% 
   helpers.py26196%73
apps/tasks
   models.py670100% 
   serializers.py60100% 
   views.py370100% 
apps/users
   helpers.py120100% 
   models.py530100% 
   views.py240100% 
apps/workspaces
   actions.py46296%42, 126
   models.py1130100% 
   permissions.py34974%33, 59–67
   serializers.py39197%53
   signals.py260100% 
   tasks.py116992%87–91, 112, 185, 215–216
   utils.py90100% 
   views.py216896%68–71, 104, 329, 435–436
apps/workspaces/apis/advanced_settings
   serializers.py78396%248, 251, 254
   triggers.py100100% 
   views.py110100% 
apps/workspaces/apis/errors
   serializers.py200100% 
   views.py150100% 
apps/workspaces/apis/export_settings
   serializers.py98397%256, 259, 262
   triggers.py27293%33–34
   views.py110100% 
apps/workspaces/apis/import_settings
   serializers.py1401788%231–237, 242–248, 256, 274, 277, 305, 308–309, 317
   triggers.py47198%31
   views.py330100% 
workers/export
   actions.py110100% 
   worker.py46589%37–38, 66–67, 100
TOTAL684354092% 

Tests Skipped Failures Errors Time
345 0 💤 0 ❌ 0 🔥 39.801s ⏱️

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

♻️ Duplicate comments (2)
apps/sage_intacct/views.py (1)

291-299: ⚠️ Potential issue

Same critical variable scoping issue exists here.

This has the same problem as the previous exception handler - sage_intacct_credentials may not be defined if credential retrieval fails.

Apply the same fix pattern as suggested for the previous handler.

🧰 Tools
🪛 Pylint (3.3.7)

[convention] 296-296: Formatting a regular string which could be an f-string

(C0209)

apps/sage_intacct/tasks.py (1)

819-819: 🛠️ Refactor suggestion

Remove redundant credential retrieval.

This line retrieves credentials that were already fetched at line 792 in the same function scope. This is unnecessary and should be removed to avoid redundant database queries.

-            sage_intacct_credentials = SageIntacctCredential.get_active_sage_intacct_credentials(expense_group.workspace_id)
🧰 Tools
🪛 Pylint (3.3.7)

[convention] 819-819: Line too long (124/120)

(C0301)

🧹 Nitpick comments (4)
apps/workspaces/views.py (1)

331-331: Use f-string for better readability.

Replace the old-style string formatting with an f-string as suggested by the static analysis tool.

-                    'message': 'Sage Intacct credentials does not exist workspace_id - %s' % workspace_id
+                    'message': f'Sage Intacct credentials does not exist workspace_id - {workspace_id}'
🧰 Tools
🪛 Pylint (3.3.7)

[convention] 331-331: Formatting a regular string which could be an f-string

(C0209)

apps/mappings/tasks.py (2)

112-117: Enhanced exception handling looks good with minor logging improvements needed.

The separation of exception handling for DoesNotExist and InvalidTokenError is a good improvement. The credential invalidation on token errors is properly implemented.

However, consider addressing the static analysis hints for better logging practices:

-        logger.info('Sage Intacct credentials does not exist workspace_id - {0}'.format(workspace_id))
+        logger.info('Sage Intacct credentials does not exist workspace_id - %s', workspace_id)
-        logger.info('Invalid Sage Intacct Token Error for workspace_id - {0}'.format(workspace_id))
+        logger.info('Invalid Sage Intacct Token Error for workspace_id - %s', workspace_id)
🧰 Tools
🪛 Pylint (3.3.7)

[warning] 113-113: Use lazy % formatting in logging functions

(W1202)


[convention] 113-113: Formatting a regular string which could be an f-string

(C0209)


[warning] 117-117: Use lazy % formatting in logging functions

(W1202)


[convention] 117-117: Formatting a regular string which could be an f-string

(C0209)


225-225: Credential retrieval standardization approved, but line is too long.

The change to use get_active_sage_intacct_credentials is correct and consistent with the overall improvement.

-        sage_intacct_credentials: SageIntacctCredential = SageIntacctCredential.get_active_sage_intacct_credentials(workspace_id)
+        sage_intacct_credentials: SageIntacctCredential = (
+            SageIntacctCredential.get_active_sage_intacct_credentials(workspace_id)
+        )
🧰 Tools
🪛 Pylint (3.3.7)

[convention] 225-225: Line too long (125/120)

(C0301)

apps/sage_intacct/tasks.py (1)

1620-1626: Improved exception handling in status checking function.

The enhanced exception handling in check_sage_intacct_object_status properly separates different types of errors and includes credential invalidation for token errors. However, the line is quite long and could benefit from formatting.

-    except (SageIntacctCredential.DoesNotExist, NoPrivilegeError):
-        logger.info('SageIntacct credentials does not exist - %s or Insufficient permission to access the requested module', workspace_id)
+    except (SageIntacctCredential.DoesNotExist, NoPrivilegeError):
+        logger.info(
+            'SageIntacct credentials does not exist - %s or Insufficient permission to access the requested module',
+            workspace_id
+        )
🧰 Tools
🪛 Pylint (3.3.7)

[convention] 1621-1621: Line too long (138/120)

(C0301)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d72b25b and 2caed13.

📒 Files selected for processing (8)
  • apps/mappings/imports/modules/base.py (1 hunks)
  • apps/mappings/tasks.py (4 hunks)
  • apps/sage_intacct/tasks.py (17 hunks)
  • apps/sage_intacct/views.py (5 hunks)
  • apps/workspaces/models.py (1 hunks)
  • apps/workspaces/tasks.py (1 hunks)
  • apps/workspaces/views.py (2 hunks)
  • tests/sql_fixtures/reset_db_fixtures/reset_db.sql (4 hunks)
✅ Files skipped from review due to trivial changes (1)
  • tests/sql_fixtures/reset_db_fixtures/reset_db.sql
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/workspaces/tasks.py
  • apps/workspaces/models.py
  • apps/mappings/imports/modules/base.py
🧰 Additional context used
🧬 Code Graph Analysis (3)
apps/sage_intacct/views.py (2)
fyle_intacct_api/utils.py (1)
  • invalidate_sage_intacct_credentials (30-38)
apps/workspaces/models.py (2)
  • SageIntacctCredential (166-190)
  • get_active_sage_intacct_credentials (184-190)
apps/mappings/tasks.py (2)
fyle_intacct_api/utils.py (1)
  • invalidate_sage_intacct_credentials (30-38)
apps/workspaces/models.py (2)
  • SageIntacctCredential (166-190)
  • get_active_sage_intacct_credentials (184-190)
apps/workspaces/views.py (1)
apps/workspaces/models.py (2)
  • SageIntacctCredential (166-190)
  • get_active_sage_intacct_credentials (184-190)
🪛 Pylint (3.3.7)
apps/sage_intacct/views.py

[convention] 240-240: Formatting a regular string which could be an f-string

(C0209)


[error] 245-245: Using variable 'sage_intacct_credentials' before assignment

(E0601)


[convention] 249-249: Formatting a regular string which could be an f-string

(C0209)


[convention] 287-287: Formatting a regular string which could be an f-string

(C0209)


[convention] 296-296: Formatting a regular string which could be an f-string

(C0209)

apps/mappings/tasks.py

[warning] 113-113: Use lazy % formatting in logging functions

(W1202)


[convention] 113-113: Formatting a regular string which could be an f-string

(C0209)


[warning] 117-117: Use lazy % formatting in logging functions

(W1202)


[convention] 117-117: Formatting a regular string which could be an f-string

(C0209)


[convention] 225-225: Line too long (125/120)

(C0301)

apps/sage_intacct/tasks.py

[convention] 819-819: Line too long (124/120)

(C0301)


[convention] 1360-1360: Line too long (122/120)

(C0301)


[convention] 1621-1621: Line too long (138/120)

(C0301)


[convention] 1858-1858: Line too long (125/120)

(C0301)

apps/workspaces/views.py

[convention] 331-331: Formatting a regular string which could be an f-string

(C0209)

🔇 Additional comments (10)
apps/workspaces/views.py (1)

382-382: Excellent improvement for credential retrieval.

The change to use SageIntacctCredential.get_active_sage_intacct_credentials(workspace.id) is a great improvement as it properly filters out expired credentials, aligning with the new credential expiration tracking feature.

This ensures that only active (non-expired) credentials are returned, which is consistent with the broader refactor to handle credential expiration explicitly across the codebase.

apps/mappings/tasks.py (2)

13-13: LGTM! Good addition of the credential invalidation utility.

The import of invalidate_sage_intacct_credentials supports the enhanced credential management pattern being implemented across the codebase.


95-95: Excellent improvement to credential retrieval.

Using get_active_sage_intacct_credentials ensures only non-expired credentials are retrieved, which aligns with the enhanced credential management strategy.

apps/sage_intacct/views.py (2)

16-16: LGTM! Consistent with the credential management improvements.

The import aligns with the pattern being implemented across the codebase.


225-225: Good standardization of credential retrieval.

Both instances correctly use the new get_active_sage_intacct_credentials method to ensure only active credentials are retrieved.

Also applies to: 267-267

apps/sage_intacct/tasks.py (5)

14-14: LGTM! Consistent import for credential management.

The import supports the enhanced credential invalidation pattern implemented throughout this file.


278-278: Excellent standardization of credential retrieval across all functions.

All credential retrieval calls have been consistently updated to use get_active_sage_intacct_credentials, which ensures only non-expired credentials are used. This is a significant improvement to the credential management strategy.

Also applies to: 622-622, 792-792, 963-963, 1123-1123, 1360-1360, 1490-1490, 1618-1618, 1856-1856


721-735: Enhanced exception handling pattern looks excellent.

The separation of NoPrivilegeError and InvalidTokenError into distinct exception blocks with proper credential invalidation is a significant improvement. This provides better error categorization and ensures credentials are properly invalidated when tokens become invalid.

Also applies to: 897-909, 1056-1068, 1216-1228


1420-1420: Proper credential invalidation in payment functions.

The addition of credential invalidation calls in the payment creation functions (create_ap_payment and create_sage_intacct_reimbursement) ensures consistent error handling when token errors occur during payment processing.

Also applies to: 1552-1552


1855-1862: Good addition of exception handling for vendor search function.

The try-except block properly handles missing credentials when searching for vendors. The exception handling is appropriate and follows the established pattern.

🧰 Tools
🪛 Pylint (3.3.7)

[convention] 1858-1858: Line too long (125/120)

(C0301)

Comment on lines +244 to 252
except InvalidTokenError:
invalidate_sage_intacct_credentials(workspace.id, sage_intacct_credentials)
logger.info('Invalid Sage Intact Token for workspace_id - %s', kwargs['workspace_id'])
return Response(
data={
'message': 'Invalid Sage Intact Token for workspace_id - %s' % kwargs['workspace_id']
},
status=status.HTTP_400_BAD_REQUEST
)
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

Critical issue: Variable used before assignment in InvalidTokenError handler.

The sage_intacct_credentials variable is referenced in the InvalidTokenError exception handler but may not be assigned if an exception occurs during credential retrieval.

         try:
             workspace = Workspace.objects.get(pk=kwargs['workspace_id'])
             sage_intacct_credentials = SageIntacctCredential.get_active_sage_intacct_credentials(workspace.id)
+        except SageIntacctCredential.DoesNotExist:
+            logger.info('Sage Intacct credentials not found workspace_id - %s', kwargs['workspace_id'])
+            return Response(
+                data={
+                    'message': 'Sage Intacct credentials not found workspace_id - %s' % kwargs['workspace_id']
+                },
+                status=status.HTTP_400_BAD_REQUEST
+            )
+        
+        try:
             async_task(
                 'apps.sage_intacct.helpers.check_interval_and_sync_dimension',
                 kwargs['workspace_id'],
             )
             return Response(
                 status=status.HTTP_200_OK
             )
-        except SageIntacctCredential.DoesNotExist:
-            logger.info('Sage Intacct credentials not found workspace_id - %s', kwargs['workspace_id'])
-            return Response(
-                data={
-                    'message': 'Sage Intacct credentials not found workspace_id - %s' % kwargs['workspace_id']
-                },
-                status=status.HTTP_400_BAD_REQUEST
-            )
         except InvalidTokenError:
             invalidate_sage_intacct_credentials(workspace.id, sage_intacct_credentials)
📝 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
except InvalidTokenError:
invalidate_sage_intacct_credentials(workspace.id, sage_intacct_credentials)
logger.info('Invalid Sage Intact Token for workspace_id - %s', kwargs['workspace_id'])
return Response(
data={
'message': 'Invalid Sage Intact Token for workspace_id - %s' % kwargs['workspace_id']
},
status=status.HTTP_400_BAD_REQUEST
)
try:
workspace = Workspace.objects.get(pk=kwargs['workspace_id'])
sage_intacct_credentials = SageIntacctCredential.get_active_sage_intacct_credentials(workspace.id)
except SageIntacctCredential.DoesNotExist:
logger.info('Sage Intacct credentials not found workspace_id - %s', kwargs['workspace_id'])
return Response(
data={
'message': 'Sage Intacct credentials not found workspace_id - %s' % kwargs['workspace_id']
},
status=status.HTTP_400_BAD_REQUEST
)
try:
async_task(
'apps.sage_intacct.helpers.check_interval_and_sync_dimension',
kwargs['workspace_id'],
)
return Response(
status=status.HTTP_200_OK
)
except InvalidTokenError:
invalidate_sage_intacct_credentials(workspace.id, sage_intacct_credentials)
logger.info('Invalid Sage Intact Token for workspace_id - %s', kwargs['workspace_id'])
return Response(
data={
'message': 'Invalid Sage Intact Token for workspace_id - %s' % kwargs['workspace_id']
},
status=status.HTTP_400_BAD_REQUEST
)
🧰 Tools
🪛 Pylint (3.3.7)

[error] 245-245: Using variable 'sage_intacct_credentials' before assignment

(E0601)


[convention] 249-249: Formatting a regular string which could be an f-string

(C0209)

🤖 Prompt for AI Agents
In apps/sage_intacct/views.py around lines 244 to 252, the variable
sage_intacct_credentials is used in the InvalidTokenError exception handler but
may not be assigned if an error occurs before its assignment. To fix this,
ensure sage_intacct_credentials is assigned before the try block or add a check
to confirm it is assigned before calling invalidate_sage_intacct_credentials.
Alternatively, restructure the code to retrieve and assign
sage_intacct_credentials before the try-except block so it is always defined
when the exception handler runs.

@ruuushhh ruuushhh temporarily deployed to CI Environment June 2, 2025 18:46 — with GitHub Actions Inactive
Copy link

github-actions bot commented Jun 2, 2025

Coverage

Coverage Report
FileStmtsMissCoverMissing
apps
   exceptions.py471177%27, 33, 39–44, 50, 56–61, 67, 73, 79–80
apps/fyle
   actions.py117794%212, 261, 317–321
   constants.py10100% 
   helpers.py2581893%127, 229–245, 290, 504, 586, 593–596, 599–600
   models.py3362393%313–315, 319–321, 325–327, 331–333, 337–339, 343–345, 406, 512–545, 568, 585
   queue.py280100% 
   serializers.py410100% 
   signals.py50688%28, 61–62, 75–77
   tasks.py1946566%160–161, 164–165, 168–169, 191–201, 213–218, 239, 268–287, 339–386
   views.py1881592%96–103, 271, 356–357, 390–391, 416–420, 513–517
apps/internal
   actions.py21290%18–19
   views.py40198%37
apps/mappings
   constants.py10100% 
   exceptions.py66395%69–71
   helpers.py200100% 
   models.py700100% 
   serializers.py100100% 
   signals.py84792%85–87, 122, 169–174
   tasks.py1071982%112–126, 245, 248, 251, 256–263
   utils.py360100% 
   views.py51296%35–36
apps/mappings/imports
   schedules.py150100% 
apps/mappings/imports/modules
   base.py153199%122
   categories.py64592%112–113, 123, 125, 157
   cost_centers.py55591%91–92, 103, 105, 139
   expense_custom_fields.py69396%60–65
   merchants.py260100% 
   projects.py61493%94–95, 111, 147
   tax_groups.py150100% 
apps/sage_intacct
   dependent_fields.py2644085%307, 343–346, 375–384, 386–387, 389–391, 393–394, 396–397, 401–403, 447, 467, 507–509, 555–557, 605–611, 615–616
   helpers.py57296%71–72
   models.py7893596%70–71, 221, 328, 335, 379, 400–417, 476, 538, 544, 579, 749, 1066–1069, 1236–1256, 1346, 1778
   queue.py1901990%77–80, 126, 142, 150, 179–182, 244–247, 309–312
   serializers.py310100% 
   tasks.py100810290%100–101, 127–130, 164, 200–201, 205–208, 210, 213, 263, 297–298, 443, 449, 457, 612–613, 633–634, 680–681, 782–783, 850–851, 898–901, 954–955, 1011–1012, 1057–1060, 1113–1114, 1170–1171, 1299–1300, 1366–1375, 1417–1441, 1560–1564, 1620–1626, 1712–1722, 1725, 1759–1761, 1795–1798, 1860–1864, 1873
   utils.py7886891%152, 382–383, 388, 401–402, 410–411, 474–475, 549–553, 657, 897–920, 1333–1355, 1378–1388, 1616–1620, 1648–1652, 1680–1684, 1726–1730, 1783–1787, 1807, 1816, 1825, 1834–1835, 1846, 1869–1871, 2028
   views.py1191687%195–197, 202–203, 209–210, 244–247, 271, 291–294
apps/sage_intacct/errors
   errors.py20100% 
   helpers.py26196%73
apps/tasks
   models.py670100% 
   serializers.py60100% 
   views.py370100% 
apps/users
   helpers.py120100% 
   models.py530100% 
   views.py240100% 
apps/workspaces
   actions.py46296%42, 126
   models.py1130100% 
   permissions.py34974%33, 59–67
   serializers.py39197%53
   signals.py260100% 
   tasks.py116992%87–91, 112, 185, 215–216
   utils.py90100% 
   views.py214797%68–71, 104, 428–429
apps/workspaces/apis/advanced_settings
   serializers.py78396%248, 251, 254
   triggers.py100100% 
   views.py110100% 
apps/workspaces/apis/errors
   serializers.py200100% 
   views.py150100% 
apps/workspaces/apis/export_settings
   serializers.py98397%256, 259, 262
   triggers.py27293%33–34
   views.py110100% 
apps/workspaces/apis/import_settings
   serializers.py1401788%231–237, 242–248, 256, 274, 277, 305, 308–309, 317
   triggers.py47198%31
   views.py330100% 
workers/export
   actions.py110100% 
   worker.py46589%37–38, 66–67, 100
TOTAL684153992% 

Tests Skipped Failures Errors Time
345 0 💤 0 ❌ 0 🔥 43.475s ⏱️

@ruuushhh ruuushhh merged commit ffd0d8e into master Jun 2, 2025
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
size/L Large PR
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants