Skip to content

feat: add support for intacct attribute count api#842

Merged
ruuushhh merged 8 commits intomasterfrom
intacct-attribute-count
Nov 24, 2025
Merged

feat: add support for intacct attribute count api#842
ruuushhh merged 8 commits intomasterfrom
intacct-attribute-count

Conversation

@ruuushhh
Copy link
Contributor

@ruuushhh ruuushhh commented Nov 21, 2025

Description

feat: add support for intacct attribute count api

Clickup

https://app.clickup.com/

Summary by CodeRabbit

  • New Features

    • Per-workspace attribute counts with a new read API and automatic initialization for new and existing workspaces.
  • Refactor

    • Unified sync gating using consolidated limits and centralized per-attribute tracking to control sync behavior.
  • Chores

    • Bumped Sage Intacct SDK to 1.28.0; added DB migration to persist attribute counts.
  • Tests

    • Added/updated tests covering the model, API view, sync gating, and workspace initialization.

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 21, 2025

Walkthrough

Adds a per-workspace SageIntacctAttributesCount model and migration, backfills rows via an internal RunSQL migration, initializes records on workspace creation, exposes a RetrieveAPIView and URL for counts, refactors sync gating to unified scalar limits with persisted per-attribute counts (including UDD), updates SQL fixtures, expands tests, and bumps sageintacctsdk.

Changes

Cohort / File(s) Summary
DB migration & fixture
apps/sage_intacct/migrations/0032_sageintacctattributescount.py, tests/sql_fixtures/reset_db_fixtures/reset_db.sql
Creates sage_intacct_attributes_count table with many integer *_count fields, user_defined_dimensions_details JSON, timestamps, workspace_id one-to-one FK (PROTECT), db table name set, indexes/sequences and related SQL fixture additions.
Internal backfill migration
apps/internal/migrations/0026_auto_generated_sql.py
Adds a RunSQL migration to insert sage_intacct_attributes_count rows for workspaces lacking them; includes reversible reverse_sql to delete inserted rows.
Model & updater
apps/sage_intacct/models.py
Adds SageIntacctAttributesCount model (db_table=sage_intacct_attributes_count) with fields for many attribute counts, user_defined_dimensions_details JSONField, timestamps, one-to-one workspace relation, and static update_attribute_count(workspace_id, attribute_type, count). Duplicate class insertion observed in diff.
Serializers
apps/sage_intacct/serializers.py
Adds SageIntacctAttributesCountSerializer (ModelSerializer) exposing model fields except user_defined_dimensions_details.
Views & URLs
apps/sage_intacct/views.py, apps/sage_intacct/urls.py
Adds SageIntacctAttributesCountView (DRF RetrieveAPIView, lookup workspace_id) and routes path('attributes_count/', SageIntacctAttributesCountView.as_view()); import adjustments.
Sync utils refactor
apps/sage_intacct/utils.py
Replaces per-type SYNC_UPPER_LIMIT map with scalar SYNC_UPPER_LIMIT = 30000 and COST_TYPES_LIMIT = 500000; changes is_sync_allowed signature to is_sync_allowed(attribute_count, attribute_type=None); persist per-attribute counts via SageIntacctAttributesCount.update_attribute_count(...) before gating; UDD counts stored in JSON field.
Workspace integration
apps/workspaces/views.py
On workspace creation, creates corresponding SageIntacctAttributesCount record (SageIntacctAttributesCount.objects.create(workspace_id=workspace.id)).
Tests
tests/**/*.py (multiple files)
Adds/updates tests covering model, utils, and view behavior: new model test, view test(s) for attributes_count endpoint, expanded mocks for count APIs, persistence assertions. Duplicate test insertions and duplicated test function definitions observed and should be deduplicated.
Requirements
requirements.txt
Bumps sageintacctsdk from 1.27.4 to 1.28.0.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Client
  participant API as DRF: SageIntacctAttributesCountView
  participant DB as DB: sage_intacct_attributes_count

  Client->>API: GET /sage-intacct/attributes_count/?workspace_id={id}
  API->>DB: SELECT * FROM sage_intacct_attributes_count WHERE workspace_id={id}
  DB-->>API: AttributesCount row (or 404)
  API-->>Client: 200 OK (serialized counts) / 404 Not Found
Loading
sequenceDiagram
  autonumber
  participant Sync as SyncWorker (utils)
  participant Intacct as Sage Intacct API
  participant Model as SageIntacctAttributesCount
  participant DB as DB

  rect rgba(230,245,230,0.6)
  note right of Sync: Per-attribute flow: count → persist → gate → process/skip
  Sync->>Intacct: Fetch attribute count (e.g., items)
  Intacct-->>Sync: count = N
  Sync->>Model: update_attribute_count(workspace_id, attribute_type, N)
  Model->>DB: INSERT/UPDATE count & UDD details
  Sync->>Sync: is_sync_allowed(N, attribute_type)
  alt allowed
    Sync->>Intacct: Fetch paginated records
    Intacct-->>Sync: Records
    Sync->>DB: Upsert records
  else over limit
    Sync-->>Sync: Log and skip processing
  end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Areas needing extra attention:
    • Duplicate SageIntacctAttributesCount class occurrence in apps/sage_intacct/models.py.
    • Ensure is_sync_allowed signature is consistently updated across all callers.
    • Verify migration schema matches model (JSON defaults, timestamps, one-to-one workspace FK with PROTECT).
    • Deduplicate duplicated tests (identical test functions added twice) and confirm test fixtures/teardown for new table.
    • Review internal RunSQL reverse logic for correctness and idempotence.

Suggested reviewers

  • Hrishabh17

Poem

I counted carrots in the code today,
Little sums that hop and softly stay.
Limits guard the garden, tidy and neat,
Syncs nibble gently — no overflow to meet.
Thump-thump! says the rabbit — stats snug and sweet. 🥕🐇

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request description is largely incomplete. It only restates the title and provides an incomplete ClickUp link without further details about the feature, rationale, or implementation. Expand the description with feature rationale, key changes (new model, API endpoint, sync logic updates), and complete the ClickUp link with the actual ticket number or URL.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add support for intacct attribute count api' clearly and accurately summarizes the main change—adding a new API feature for tracking Intacct attribute counts across multiple count types and models.
Docstring Coverage ✅ Passed Docstring coverage is 95.74% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch intacct-attribute-count

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

❤️ Share

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

@github-actions github-actions bot added the size/L Large PR label Nov 21, 2025
@github-actions
Copy link

Coverage

Coverage Report
FileStmtsMissCoverMissing
apps
   exceptions.py471177%27, 33, 39–44, 50, 56–61, 67, 73, 79–80
apps/fyle
   actions.py124794%214, 263, 319–323
   constants.py10100% 
   helpers.py2352291%50–57, 66, 146–162, 376, 470, 477–480, 483–484
   models.py3362194%313–315, 319–321, 325–327, 331–333, 337–339, 343–345, 406, 516–537, 568, 585
   queue.py470100% 
   serializers.py410100% 
   signals.py35683%22, 55–56, 69–71
   tasks.py4487384%154–155, 158–159, 162–163, 185–186, 220–230, 242–247, 290–293, 314–318, 328–336, 371–372, 402–449, 733–737
   views.py1721591%84–91, 145, 245–246, 290–291, 316–320, 413–417
apps/internal
   actions.py39295%26–27
   helpers.py361364%18–35
   serializers.py300100% 
   views.py97199%39
apps/internal/services
   e2e_setup.py590100% 
   fixture_factory.py59395%218–220
apps/mappings
   constants.py20100% 
   exceptions.py66395%69–71
   helpers.py300100% 
   models.py500100% 
   schedules.py150100% 
   serializers.py100100% 
   signals.py1071289%84–86, 121, 174–179, 185–189, 195–198
   tasks.py1683182%105–106, 126, 190–207, 237, 240, 243, 248–258, 297–299, 310, 382
   utils.py340100% 
   views.py46491%34–35, 114–115
apps/sage_intacct
   actions.py29486%21–22, 49–50
   dependent_fields.py2704085%315, 351–354, 383–392, 394–395, 397–399, 401–402, 404–405, 409–411, 455, 475, 515–517, 563–565, 613–619, 623–624
   helpers.py40295%51–52
   models.py9008491%58–59, 209, 316, 323, 367, 388–405, 464, 476, 526, 532, 567, 617–619, 633–646, 657, 693, 812, 851, 1047, 1138–1141, 1211, 1312–1332, 1410, 1426, 1834–1888, 1919, 1963–1999
   queue.py1631690%45, 53, 58, 105, 127–130, 170, 184, 199–202, 237, 258–261, 296, 318–321
   serializers.py350100% 
   tasks.py103913687%77–80, 114, 150–151, 155–158, 160, 163, 213, 247–248, 394, 400, 408, 572–573, 593–594, 640–641, 645, 664, 679, 684, 689, 696, 702, 716, 753–754, 821–822, 826, 847, 862, 868, 871–874, 881, 895–899, 936–937, 993–994, 996, 1017, 1032, 1038, 1041–1044, 1051, 1065–1069, 1105–1106, 1162–1163, 1166, 1187, 1202, 1208, 1214, 1221, 1227, 1241, 1293–1294, 1360–1369, 1411–1435, 1554–1558, 1614–1620, 1706–1716, 1719, 1728–1729, 1763–1765, 1799–1802, 1870–1874, 1883
   utils.py87618279%96, 108, 111, 114, 189–193, 241–243, 298–300, 349–351, 408–435, 443–473, 481–510, 526–528, 536–537, 560–562, 617–619, 669–671, 714–718, 759–792, 800–838, 846, 850–898, 928–930, 976–978, 1020–1022, 1065–1067, 1165, 1181–1187, 1196–1197, 1288–1328, 1672–1682, 1910–1914, 1942–1946, 1974–1978, 2020–2024, 2077–2081, 2101, 2110, 2119, 2128–2129, 2140, 2163–2165
   views.py1433377%126–128, 133–134, 140–141, 184–187, 256–286, 297–312, 325–329
apps/sage_intacct/errors
   errors.py20100% 
   helpers.py26196%73
apps/tasks
   models.py680100% 
   serializers.py60100% 
   views.py370100% 
apps/users
   helpers.py120100% 
   models.py530100% 
   views.py170100% 
apps/workspaces
   actions.py46980%65, 84–85, 103, 122–133, 147
   enums.py50100% 
   helpers.py80100% 
   models.py175498%338–340, 350–352
   permissions.py34974%33, 59–67
   serializers.py39197%53
   signals.py310100% 
   tasks.py154895%52–58, 186–193, 234, 266–267
   utils.py90100% 
   views.py262698%115–118, 484–485
apps/workspaces/apis/advanced_settings
   serializers.py80396%260, 263, 266
   triggers.py100100% 
   views.py110100% 
apps/workspaces/apis/errors
   serializers.py200100% 
   views.py150100% 
apps/workspaces/apis/export_settings
   helpers.py730100% 
   serializers.py103397%267, 270, 273
   triggers.py39295%32–33
   views.py110100% 
apps/workspaces/apis/import_settings
   serializers.py1411887%232–238, 243–249, 257–265, 283, 286, 314, 317–318, 326
   triggers.py47198%31
   views.py330100% 
fyle_integrations_imports
   dataclasses.py310100% 
   models.py230100% 
   queues.py34585%24, 99, 136, 148–149
   signals.py100100% 
   tasks.py926628%66–103, 112–118, 131–194, 198–213
fyle_integrations_imports/modules
   base.py182796%75, 91–92, 95, 224–225, 343
   categories.py1712983%79, 95, 98–99, 102–103, 270–271, 281, 294, 296, 340, 357–403
   cost_centers.py90891%131–132, 150–151, 160, 176, 178, 224
   expense_custom_fields.py121794%83–88, 247, 263, 265, 294
   merchants.py1002179%79–82, 110–128, 154–161, 177–178, 188, 201, 203, 233
   projects.py82594%107–108, 118, 142, 189
   tax_groups.py180100% 
   webhook_attributes.py127695%95, 97–99, 208–209
workers
   actions.py210100% 
   helpers.py410100% 
   worker.py56591%44–45, 77–78, 125
TOTAL859594589% 

Tests Skipped Failures Errors Time
480 0 💤 9 ❌ 0 🔥 45.741s ⏱️

@github-actions
Copy link

Coverage

Coverage Report
FileStmtsMissCoverMissing
apps
   exceptions.py471177%27, 33, 39–44, 50, 56–61, 67, 73, 79–80
apps/fyle
   actions.py124794%214, 263, 319–323
   constants.py10100% 
   helpers.py2352291%50–57, 66, 146–162, 376, 470, 477–480, 483–484
   models.py3362194%313–315, 319–321, 325–327, 331–333, 337–339, 343–345, 406, 516–537, 568, 585
   queue.py470100% 
   serializers.py410100% 
   signals.py35683%22, 55–56, 69–71
   tasks.py4487384%154–155, 158–159, 162–163, 185–186, 220–230, 242–247, 290–293, 314–318, 328–336, 371–372, 402–449, 733–737
   views.py1721591%84–91, 145, 245–246, 290–291, 316–320, 413–417
apps/internal
   actions.py39295%26–27
   helpers.py361364%18–35
   serializers.py300100% 
   views.py97199%39
apps/internal/services
   e2e_setup.py590100% 
   fixture_factory.py59395%218–220
apps/mappings
   constants.py20100% 
   exceptions.py66395%69–71
   helpers.py300100% 
   models.py500100% 
   schedules.py150100% 
   serializers.py100100% 
   signals.py1071289%84–86, 121, 174–179, 185–189, 195–198
   tasks.py1683182%105–106, 126, 190–207, 237, 240, 243, 248–258, 297–299, 310, 382
   utils.py340100% 
   views.py46491%34–35, 114–115
apps/sage_intacct
   actions.py29486%21–22, 49–50
   dependent_fields.py2704085%315, 351–354, 383–392, 394–395, 397–399, 401–402, 404–405, 409–411, 455, 475, 515–517, 563–565, 613–619, 623–624
   helpers.py40295%51–52
   models.py9008491%58–59, 209, 316, 323, 367, 388–405, 464, 476, 526, 532, 567, 617–619, 633–646, 657, 693, 812, 851, 1047, 1138–1141, 1211, 1312–1332, 1410, 1426, 1834–1888, 1919, 1963–1999
   queue.py1631690%45, 53, 58, 105, 127–130, 170, 184, 199–202, 237, 258–261, 296, 318–321
   serializers.py350100% 
   tasks.py103913687%77–80, 114, 150–151, 155–158, 160, 163, 213, 247–248, 394, 400, 408, 572–573, 593–594, 640–641, 645, 664, 679, 684, 689, 696, 702, 716, 753–754, 821–822, 826, 847, 862, 868, 871–874, 881, 895–899, 936–937, 993–994, 996, 1017, 1032, 1038, 1041–1044, 1051, 1065–1069, 1105–1106, 1162–1163, 1166, 1187, 1202, 1208, 1214, 1221, 1227, 1241, 1293–1294, 1360–1369, 1411–1435, 1554–1558, 1614–1620, 1706–1716, 1719, 1728–1729, 1763–1765, 1799–1802, 1870–1874, 1883
   utils.py87618279%96, 108, 111, 114, 189–193, 241–243, 298–300, 349–351, 408–435, 443–473, 481–510, 526–528, 536–537, 560–562, 617–619, 669–671, 714–718, 759–792, 800–838, 846, 850–898, 928–930, 976–978, 1020–1022, 1065–1067, 1165, 1181–1187, 1196–1197, 1288–1328, 1672–1682, 1910–1914, 1942–1946, 1974–1978, 2020–2024, 2077–2081, 2101, 2110, 2119, 2128–2129, 2140, 2163–2165
   views.py1433377%126–128, 133–134, 140–141, 184–187, 256–286, 297–312, 325–329
apps/sage_intacct/errors
   errors.py20100% 
   helpers.py26196%73
apps/tasks
   models.py680100% 
   serializers.py60100% 
   views.py370100% 
apps/users
   helpers.py120100% 
   models.py530100% 
   views.py170100% 
apps/workspaces
   actions.py46980%65, 84–85, 103, 122–133, 147
   enums.py50100% 
   helpers.py80100% 
   models.py175498%338–340, 350–352
   permissions.py34974%33, 59–67
   serializers.py39197%53
   signals.py310100% 
   tasks.py154895%52–58, 186–193, 234, 266–267
   utils.py90100% 
   views.py262698%115–118, 484–485
apps/workspaces/apis/advanced_settings
   serializers.py80396%260, 263, 266
   triggers.py100100% 
   views.py110100% 
apps/workspaces/apis/errors
   serializers.py200100% 
   views.py150100% 
apps/workspaces/apis/export_settings
   helpers.py730100% 
   serializers.py103397%267, 270, 273
   triggers.py39295%32–33
   views.py110100% 
apps/workspaces/apis/import_settings
   serializers.py1411887%232–238, 243–249, 257–265, 283, 286, 314, 317–318, 326
   triggers.py47198%31
   views.py330100% 
fyle_integrations_imports
   dataclasses.py310100% 
   models.py230100% 
   queues.py34585%24, 99, 136, 148–149
   signals.py100100% 
   tasks.py926628%66–103, 112–118, 131–194, 198–213
fyle_integrations_imports/modules
   base.py182796%75, 91–92, 95, 224–225, 343
   categories.py1712983%79, 95, 98–99, 102–103, 270–271, 281, 294, 296, 340, 357–403
   cost_centers.py90891%131–132, 150–151, 160, 176, 178, 224
   expense_custom_fields.py121794%83–88, 247, 263, 265, 294
   merchants.py1002179%79–82, 110–128, 154–161, 177–178, 188, 201, 203, 233
   projects.py82594%107–108, 118, 142, 189
   tax_groups.py180100% 
   webhook_attributes.py127695%95, 97–99, 208–209
workers
   actions.py210100% 
   helpers.py410100% 
   worker.py56591%44–45, 77–78, 125
TOTAL859594589% 

Tests Skipped Failures Errors Time
480 0 💤 9 ❌ 0 🔥 42.687s ⏱️

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

Caution

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

⚠️ Outside diff range comments (2)
apps/sage_intacct/utils.py (1)

228-243: Handle .count() failures and centralize count + gating behavior

This codebase calls .count() on various Sage Intacct resources (accounts, departments, expense_types, charge_card_accounts, payment_accounts, cost_types, cost_codes, projects, items, locations, expense_payment_types, employees, allocations, dimension_values, classes, customers, tax_details, vendors) without error handling. WrongParamsError and NoPrivilegeError are legitimate exceptions from the Intacct SDK—already used throughout the codebase (tasks.py, dependent_fields.py, etc.)—but they are not caught around the 18+ .count() calls in utils.py. When .count() fails due to permission restrictions or unsupported filters, the entire sync breaks instead of being handled gracefully.

The pattern is consistent across sync methods:

  1. Call .count(...)
  2. Persist via SageIntacctAttributesCount.update_attribute_count(...)
  3. Gate via is_sync_allowed(...) and potentially return early

Consider extracting a helper that wraps count, persistence, and gating with controlled exception handling. First, add NoPrivilegeError to the import line (line 16):

from sageintacctsdk.exceptions import WrongParamsError, NoPrivilegeError

Then implement the helper:

def _get_and_record_count(
    self,
    attribute_type: str,
    *,
    is_cost_type: bool = False,
    **count_kwargs: Any,
) -> int | None:
    try:
        attribute_count = self.connection.__getattribute__(attribute_type).count(**count_kwargs)
    except (WrongParamsError, NoPrivilegeError) as exc:
        logger.info(
            "Skipping %s count for workspace %s due to Intacct error: %s",
            attribute_type, self.workspace_id, exc,
        )
        return None

    SageIntacctAttributesCount.update_attribute_count(
        workspace_id=self.workspace_id,
        attribute_type=attribute_type,
        count=attribute_count,
    )

    limit_type = "cost_types" if is_cost_type else None
    if not self.is_sync_allowed(attribute_count=attribute_count, attribute_type=limit_type):
        logger.info(
            "Skipping sync of %s for workspace %s as it has %s counts which is over the limit of %s",
            attribute_type, self.workspace_id, attribute_count,
            COST_TYPES_LIMIT if is_cost_type else SYNC_UPPER_LIMIT,
        )
        return None

    return attribute_count

Then each sync_* method can early-return if this helper returns None, avoiding duplicated patterns and making error handling uniform.

apps/sage_intacct/views.py (1)

248-316: Add timeout protection and reset is_expired flag in OAuth credential flow.

The OAuth flow has two issues that should be addressed:

  1. Add timeout and JSON validation to requests.post
    The external call to settings.INTACCT_TOKEN_URI at line 305 lacks a timeout and assumes a well-formed response with a refresh_token key. This can cause hanging requests or 500 errors on malformed responses.

  2. Reset is_expired flag when updating credentials
    The update_or_create call at lines 278–283 omits is_expired from defaults. If previous authentication attempts marked credentials as expired, the flag remains True and subsequent calls to get_active_sage_intacct_credentials (which filters is_expired=False at apps/workspaces/models.py:199) will fail. The old password-based auth flow resets this flag explicitly (see apps/workspaces/views.py:376), and the new OAuth flow should do the same for consistency.

Apply the suggested diffs from the original review, ensuring both the timeout protection and is_expired: False addition are included in the update_or_create call.

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

26-38: Consider backfilling SageIntacctAttributesCount for existing workspaces

Creating a SageIntacctAttributesCount row alongside new workspaces is good, and aligns with the OneToOne link. However, older workspaces created before this change will only get a row lazily via update_attribute_count(...).get_or_create(...) paths during sync.

If the new attributes_count API is expected to work immediately for all existing workspaces (even before any sync runs), you may want a data migration or management command to backfill SageIntacctAttributesCount rows for those workspaces.

Also applies to: 123-131

apps/sage_intacct/utils.py (1)

179-195: Fix is_sync_allowed type hint to resolve RUF013 lint warning

The parameter attribute_type has an invalid type hint. It declares str but defaults to None, which violates the RUF013 lint rule:

def is_sync_allowed(self, attribute_count: int, attribute_type: str = None) -> bool:

Update to make the optionality explicit:

-    def is_sync_allowed(self, attribute_count: int, attribute_type: str = None) -> bool:
+    def is_sync_allowed(self, attribute_count: int, attribute_type: str | None = None) -> bool:

All 16 call sites are compatible: they either pass no attribute_type argument or pass a string literal ('cost_types'). Python 3.10+ is already in use (Dockerfile specifies python:3.10-slim), so PEP 604 syntax is supported.

apps/sage_intacct/views.py (2)

90-143: SageIntacctFieldsView logic looks sound; minor note on queryset usage.

The exclusion logic using chained ~Q(...) filters and distinct attribute_type/display_name pairs is consistent, and the override with DimensionDetail plus the special handling for ALLOCATION and PROJECT matches the surrounding domain model.

The only nit is that dimensions.filter(...).exists() / .get(...) is called per attribute, which is fine for expected small cardinalities but could be optimized later (e.g., by pre-building a {attribute_type: display_name} dict) if needed.


318-333: JWT decode: acceptable for parsing, but consider proper verification.

Decoding the Intacct refresh token with verify_signature=False is sufficient to parse the payload and enforce a company‑ID consistency check, but it provides no assurance that the token was actually issued by Intacct (or that it hasn’t expired/tampered).

If Intacct exposes the appropriate public keys or verification endpoint, consider moving towards proper verification (signature and expiry) when possible. If not, a brief code comment explaining why verification is disabled (e.g., “Intacct does not expose JWT verification keys; this decode is used only for cross‑checking cnyId against stored credentials”) would help future maintainers.

apps/sage_intacct/models.py (1)

2002-2050: SageIntacctAttributesCount model looks good; add safety around dynamic field updates.

The model structure (OneToOne with Workspace, per‑dimension *_count fields, and user_defined_dimensions_details JSON) is consistent with the new API and should work well for per‑workspace limits and reporting.

The update_attribute_count helper is convenient, but as implemented it assumes attribute_type always maps to an existing *_count field. If an unsupported attribute_type is ever passed (e.g., a typo or a newly added dimension without a corresponding field), save(update_fields=[field_name, ...]) will raise a FieldError.

To make this safer and easier to debug, consider guarding the dynamic update:

    @staticmethod
    def update_attribute_count(workspace_id: int, attribute_type: str, count: int) -> None:
        """
        Update attribute count for a workspace
        """
        sage_intacct_count, _ = SageIntacctAttributesCount.objects.get_or_create(
            workspace_id=workspace_id
        )
        field_name = f'{attribute_type}_count'
-        setattr(sage_intacct_count, field_name, count)
-        sage_intacct_count.save(update_fields=[field_name, 'updated_at'])
+        if not hasattr(sage_intacct_count, field_name):
+            # Optional: log instead of raising, depending on how strict you want this to be
+            raise ValueError(f'Unsupported attribute_type "{attribute_type}" for SageIntacctAttributesCount')
+
+        setattr(sage_intacct_count, field_name, count)
+        sage_intacct_count.save(update_fields=[field_name, 'updated_at'])
tests/sql_fixtures/reset_db_fixtures/reset_db.sql (1)

11992-11992: Verify necessity of updated_at index.

An index on updated_at is created for sorting/filtering by recency. Confirm this index is actually used by the API endpoint queries; if not, consider removing it to reduce maintenance and storage overhead.

apps/sage_intacct/migrations/0032_sageintacctattributescount.py (1)

18-40: Schema is sound; consider tightening count types and JSONField semantics

Overall this migration looks consistent and safe to apply. A couple of small, non‑blocking tweaks you might want to consider:

  • For all *_count fields (Lines 19–31, 33–36), using models.PositiveIntegerField instead of models.IntegerField would better reflect that these are non‑negative counters and prevent negative values from being persisted by mistake.
  • For user_defined_dimensions_details (Line 32), you currently allow both null and {} as “empty” states (null=True plus default=dict). If you don’t need to distinguish between “no data yet” and “no dimensions”, you might simplify by dropping null=True and just using {} as the canonical empty value.

If you decide to adopt this, the migration snippet could look like:

-                ('accounts_count', models.IntegerField(default=0, help_text='Number of accounts in Sage Intacct')),
+                ('accounts_count', models.PositiveIntegerField(default=0, help_text='Number of accounts in Sage Intacct')),
@@
-                ('allocations_count', models.IntegerField(default=0, help_text='Number of allocations in Sage Intacct')),
+                ('allocations_count', models.PositiveIntegerField(default=0, help_text='Number of allocations in Sage Intacct')),
@@
-                ('user_defined_dimensions_details', models.JSONField(blank=True, default=dict, help_text='Count breakdown per UDD dimension (e.g., {"DEPARTMENT": 150, "PROJECT": 500}). Not exposed via API.', null=True)),
+                ('user_defined_dimensions_details', models.JSONField(blank=True, default=dict, help_text='Count breakdown per UDD dimension (e.g., {"DEPARTMENT": 150, "PROJECT": 500}). Not exposed via API.', null=False)),

(And mirror the same types in the corresponding model definition.)

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2586fef and ae21b4c.

📒 Files selected for processing (9)
  • apps/sage_intacct/migrations/0032_sageintacctattributescount.py (1 hunks)
  • apps/sage_intacct/models.py (2 hunks)
  • apps/sage_intacct/serializers.py (2 hunks)
  • apps/sage_intacct/urls.py (1 hunks)
  • apps/sage_intacct/utils.py (23 hunks)
  • apps/sage_intacct/views.py (2 hunks)
  • apps/workspaces/views.py (2 hunks)
  • requirements.txt (1 hunks)
  • tests/sql_fixtures/reset_db_fixtures/reset_db.sql (19 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
apps/workspaces/views.py (1)
apps/sage_intacct/models.py (1)
  • SageIntacctAttributesCount (2002-2050)
apps/sage_intacct/views.py (5)
apps/sage_intacct/helpers.py (1)
  • sync_dimensions (64-98)
apps/sage_intacct/models.py (1)
  • SageIntacctAttributesCount (2002-2050)
apps/sage_intacct/serializers.py (2)
  • SageIntacctAttributesCountSerializer (78-84)
  • SageIntacctFieldSerializer (69-75)
apps/workspaces/enums.py (1)
  • CacheKeyEnum (4-10)
apps/workspaces/models.py (3)
  • Configuration (108-169)
  • SageIntacctCredential (172-199)
  • Workspace (83-105)
apps/sage_intacct/serializers.py (1)
apps/sage_intacct/models.py (19)
  • Bill (817-886)
  • ChargeCardTransaction (1378-1447)
  • ChargeCardTransactionLineitem (1450-1550)
  • SageIntacctAttributesCount (2002-2050)
  • Meta (835-836)
  • Meta (916-917)
  • Meta (1033-1034)
  • Meta (1097-1098)
  • Meta (1196-1197)
  • Meta (1258-1259)
  • Meta (1396-1397)
  • Meta (1476-1477)
  • Meta (1566-1567)
  • Meta (1613-1614)
  • Meta (1658-1659)
  • Meta (1705-1706)
  • Meta (1759-1766)
  • Meta (1903-1909)
  • Meta (2034-2035)
apps/sage_intacct/urls.py (1)
apps/sage_intacct/views.py (1)
  • SageIntacctAttributesCountView (336-343)
apps/sage_intacct/models.py (2)
apps/fyle/models.py (7)
  • DependentFieldSetting (715-742)
  • Meta (165-171)
  • Meta (281-282)
  • Meta (479-480)
  • Meta (613-615)
  • Meta (711-712)
  • Meta (741-742)
apps/workspaces/models.py (9)
  • Workspace (83-105)
  • Meta (104-105)
  • Meta (168-169)
  • Meta (189-190)
  • Meta (213-214)
  • Meta (234-235)
  • Meta (256-257)
  • Meta (300-301)
  • Meta (328-329)
apps/sage_intacct/utils.py (2)
apps/sage_intacct/models.py (2)
  • SageIntacctAttributesCount (2002-2050)
  • update_attribute_count (2038-2050)
apps/workspaces/models.py (1)
  • Workspace (83-105)
🪛 GitHub Actions: Continuous Integration
apps/sage_intacct/utils.py

[error] 1-1: Several Sage Intacct sync operations (employees/vendors/allocations/etc.) fail with WrongParamsError or NoPrivilegeError during count/sync calls, causing multiple test failures in Sage Intacct integration utilities.

🪛 Pylint (4.0.3)
apps/sage_intacct/migrations/0032_sageintacctattributescount.py

[error] 1-1: Unrecognized option found: suggestion-mode, no-space-check

(E0015)


[refactor] 1-1: Useless option value for '--disable', 'print-statement' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'parameter-unpacking' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'unpacking-in-except' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'old-raise-syntax' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'backtick' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'import-star-module-level' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'apply-builtin' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'basestring-builtin' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'buffer-builtin' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'cmp-builtin' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'coerce-builtin' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'execfile-builtin' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'file-builtin' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'long-builtin' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'raw_input-builtin' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'reduce-builtin' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'standarderror-builtin' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'unicode-builtin' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'xrange-builtin' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'coerce-method' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'delslice-method' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'getslice-method' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'setslice-method' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'no-absolute-import' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'old-division' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'dict-iter-method' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'dict-view-method' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'next-method-called' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'metaclass-assignment' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'indexing-exception' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'raising-string' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'reload-builtin' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'oct-method' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'hex-method' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'nonzero-method' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'cmp-method' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'input-builtin' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'round-builtin' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'intern-builtin' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'unichr-builtin' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'map-builtin-not-iterating' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'zip-builtin-not-iterating' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'range-builtin-not-iterating' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'filter-builtin-not-iterating' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'using-cmp-argument' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'div-method' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'idiv-method' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'rdiv-method' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'exception-message-attribute' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'invalid-str-codec' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'sys-max-int' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'bad-python3-import' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'deprecated-string-function' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'deprecated-str-translate-call' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'deprecated-itertools-function' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'deprecated-types-field' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'next-method-defined' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'dict-items-not-iterating' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'dict-keys-not-iterating' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'dict-values-not-iterating' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'deprecated-operator-function' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'deprecated-urllib-function' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'xreadlines-attribute' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'deprecated-sys-function' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'exception-escape' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'comprehension-escape' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'no-self-use' was moved to an optional extension, see https://pylint.readthedocs.io/en/latest/whatsnew/2/2.14/summary.html#removed-checkers.

(R0022)

🪛 Ruff (0.14.5)
apps/sage_intacct/serializers.py

84-84: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

apps/sage_intacct/utils.py

179-179: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)

apps/sage_intacct/migrations/0032_sageintacctattributescount.py

9-12: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


14-45: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

🔇 Additional comments (22)
apps/sage_intacct/utils.py (1)

903-961: UDD count capture and persistence looks consistent

Capturing per‑UDD counts into udd_counts and persisting to SageIntacctAttributesCount.user_defined_dimensions_details after the loop is a good approach; it keeps all per‑dimension tallies in a single JSON field and does not expose them via the public serializer.

The generic exception handler around each UDD sync block will prevent a single problematic dimension from blocking others, and the final get_or_create ensures the backing row exists even if no other sync path has touched it yet.

apps/sage_intacct/serializers.py (1)

78-84: Convert exclude list to tuple to resolve RUF012

The suggested fix is correct. Changing exclude = ['user_defined_dimensions_details'] to exclude = ('user_defined_dimensions_details',) will resolve the RUF012 warning while maintaining identical DRF behavior. Immutable tuples are the preferred approach for static Meta.exclude/fields configurations.

-        exclude = ['user_defined_dimensions_details']
+        exclude = ('user_defined_dimensions_details',)
requirements.txt (1)

23-23: Version 1.28.0 does not exist in public sageintacctsdk releases

The review comment references compatibility concerns for sageintacctsdk 1.28.0, but web search confirms the latest publicly available version on PyPI/GitHub is 1.26.0 (released January 17, 2025). Additionally, tests/integration_tests/requirements.txt pins version 1.27.2, which also does not appear in public releases. This suggests either internal pre-release versions or a version specification error in requirements.txt.

To proceed with a meaningful compatibility review, clarify:

  • Whether 1.28.0 is an internal/custom build or if the version number should be corrected to an existing release (1.26.0 or earlier)
  • Why integration tests use a different version (1.27.2) than main code
apps/sage_intacct/views.py (3)

336-343: SageIntacctAttributesCountView wiring is consistent with the new model.

Using RetrieveAPIView with:

  • queryset = SageIntacctAttributesCount.objects.all()
  • lookup_field = 'workspace_id'
  • lookup_url_kwarg = 'workspace_id'

is appropriate for a OneToOneField relation to Workspace and matches how the model exposes workspace_id as a column. Together with SageIntacctAttributesCountSerializer (excluding user_defined_dimensions_details), this gives a clean, per‑workspace counts endpoint.


146-193: Verification confirms function signature is compatible with the call site.

The function at fyle_intacct_api/utils.py:111 has signature invalidate_sage_intacct_credentials(workspace_id, sage_intacct_credentials=None). The call at line 185 in apps/sage_intacct/views.py correctly passes both arguments. The second parameter is optional, so this usage pattern is valid and consistent with other call sites throughout the codebase.


195-245: Incorrect concern about helper signature; clarify cache behavior design intent.

Concern #1 about invalidate_sage_intacct_credentials is incorrect. The function signature def invalidate_sage_intacct_credentials(workspace_id, sage_intacct_credentials=None) explicitly supports both one- and two-argument calls; the second parameter is optional. The single-argument call at line 238 is valid and consistent.

Concern #2 about cache behavior remains valid: because both automatic and manual refreshes share the same 5-minute cache key, a recent full sync will suppress a subsequent manual dimensions_to_sync request (even with specific dimensions). Verify whether this blocking behavior is intentional design or should distinguish between manual and automatic triggers.

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

178-182: Cleanup procedure correctly handles new table.

The deletion logic for intacct_sync_timestamps follows existing patterns and properly removes workspace-scoped records.


3432-3452: Table schema is well‑structured.

The intacct_sync_timestamps table properly tracks sync timestamps for each attribute type with nullable fields, mandatory audit columns, and a workspace FK.


4950-4957: Permission entries correctly registered.

New model permissions follow standard Django patterns with sequential IDs for both IntacctSyncedTimestamp and SageIntacctAttributesCount.


6080-6081: Content type mappings are correctly registered.

Model references use proper app labels and lowercase names; IDs are sequential and align with permission registrations.


6358-6361: Migration history entries are correctly sequenced.

New migrations are properly tracked with sequential IDs, consistent timestamps, and logical ordering across apps (workspaces → internal → sage_intacct).


9863-9865: Sync timestamp tracking is properly initialized.

The fixture seeds one record for the default workspace with all sync timestamps NULL, indicating no syncs have occurred yet.


10265-10266: Attribute count table is intentionally empty at initialization.

Unlike intacct_sync_timestamps, the sage_intacct_attributes_count fixture contains no initial data. Confirm this is intentional (likely seeded on first API call or via a separate command) and document the initialization contract.


10380-10380: Permission sequence setval is correct.

Sequence value (228) correctly reflects total permissions including the 8 new entries added.


10429-10429: Sequence values correctly reflect new migrations and content types.

Both django_content_type_id_seq (57) and django_migrations_id_seq (271) are properly synchronized with the 2 new models and 4 new migrations.

Also applies to: 10436-10436


10583-10583: Sync timestamp sequence setval is consistent with seeded data.

Sequence value (1, true) correctly reflects the one initialized record.


10632-10632: Attributes count sequence setval is consistent with empty initial data.

Sequence value (1, false) correctly indicates no rows yet; next insert will use ID 1.


11230-11231: Primary key constraint is properly defined.

Standard PK constraint on id column for intacct_sync_timestamps.


11334-11343: Constraints properly enforce one-to-one workspace relationship.

Primary key on id and unique constraint on workspace_id correctly ensure one attributes-count record per workspace.


11964-11964: Workspace index enables efficient per-workspace queries.

BTREE index on workspace_id supports filtering by workspace.


12555-12556: Foreign key constraint properly references workspace.

FK on workspace_idworkspaces(id) with deferred constraint checking allows bulk operations.


12643-12644: Foreign key constraint properly references workspace.

FK on workspace_idworkspaces(id) with deferred constraint checking ensures referential integrity for the one-to-one workspace relationship.

Comment on lines 5 to 21
from apps.sage_intacct.views import (
SageIntacctFieldsView,
AuthorizationCodeView,
DestinationAttributesView,
SyncSageIntacctDimensionView,
RefreshSageIntacctDimensionView,
PaginatedDestinationAttributesView,
AuthorizationCodeView
RefreshSageIntacctDimensionView,
SageIntacctAttributesCountView,
SageIntacctFieldsView,
SyncSageIntacctDimensionView,
)


sage_intacct_app_path = [
path('sage_intacct_fields/', SageIntacctFieldsView.as_view()),
path('destination_attributes/', DestinationAttributesView.as_view()),
path('paginated_destination_attributes/', PaginatedDestinationAttributesView.as_view()),
path('credentials/authorization_code/', AuthorizationCodeView.as_view()),
path('attributes_count/', SageIntacctAttributesCountView.as_view()),
]
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

🧩 Analysis chain

Fix mismatch between attributes_count URL and SageIntacctAttributesCountView lookup configuration

SageIntacctAttributesCountView is a RetrieveAPIView configured with:

  • lookup_field = 'workspace_id'
  • lookup_url_kwarg = 'workspace_id'

But the route here is:

path('attributes_count/', SageIntacctAttributesCountView.as_view()),

Without a workspace_id path parameter, DRF’s default get_object() will try to read kwargs['workspace_id'] and fail. You likely want either:

  • Add a path parameter:
-    path('attributes_count/', SageIntacctAttributesCountView.as_view()),
+    path('attributes_count/<int:workspace_id>/', SageIntacctAttributesCountView.as_view()),

or

  • Override get_object() in SageIntacctAttributesCountView to resolve the workspace_id from the authenticated user or query params instead of URL kwargs.

As it stands, this endpoint will not be callable successfully.

To confirm how the view is currently wired, you can inspect it with:


🏁 Script executed:

#!/bin/bash
rg -n "class SageIntacctAttributesCountView" -n apps/sage_intacct/views.py -C5

Length of output: 374


I need to see the complete class definition to verify whether lookup_field and lookup_url_kwarg are actually set. Let me retrieve more context:

I need to get the complete class definition to verify whether the lookup_field and lookup_url_kwarg attributes are actually set in the view:


---

🏁 Script executed:

```shell
#!/bin/bash
# Get more context around the view class definition (at least 20 lines)
sed -n '336,360p' apps/sage_intacct/views.py

Length of output: 69



---

🏁 Script executed:

```shell
#!/bin/bash
# Find the views.py file
fd -t f "views.py" | grep sage_intacct

Length of output: 26



---

🏁 Script executed:

```shell
#!/bin/bash
# Get the complete class definition starting from line 336
sed -n '336,365p' apps/sage_intacct/views.py

Length of output: 306


Fix mismatch between attributes_count URL and SageIntacctAttributesCountView lookup configuration

SageIntacctAttributesCountView is configured with:

  • lookup_field = 'workspace_id'
  • lookup_url_kwarg = 'workspace_id'

But the route is:

path('attributes_count/', SageIntacctAttributesCountView.as_view()),

Without a workspace_id path parameter, DRF's get_object() will fail trying to read kwargs['workspace_id']. Add the path parameter:

-    path('attributes_count/', SageIntacctAttributesCountView.as_view()),
+    path('attributes_count/<int:workspace_id>/', SageIntacctAttributesCountView.as_view()),

or override get_object() in the view to resolve workspace_id from the authenticated user or query params instead.

🤖 Prompt for AI Agents
In apps/sage_intacct/urls.py around lines 5 to 21, the route for
SageIntacctAttributesCountView is missing the workspace_id URL parameter
expected by the view's lookup_field/lookup_url_kwarg, causing get_object() to
raise KeyError; fix by updating the path to include the workspace_id segment
(e.g. add '<int:workspace_id>/' or appropriate converter) so
kwargs['workspace_id'] is present, or alternatively update the view to override
get_object() to derive workspace_id from the authenticated user or query
parameters and return the correct object.

Comment on lines +3 to 17
import jwt
import requests
from django.conf import settings
from django.core.cache import cache
from django.db.models import Q
from fyle_accounting_library.common_resources.enums import DimensionDetailSourceTypeEnum
from fyle_accounting_library.common_resources.models import DimensionDetail
from fyle_accounting_mappings.models import DestinationAttribute
from fyle_accounting_mappings.serializers import DestinationAttributeSerializer
from rest_framework import generics
from rest_framework.views import status
from rest_framework.exceptions import ValidationError
from rest_framework.request import Request
from rest_framework.response import Response

from rest_framework.exceptions import ValidationError
from rest_framework.views import status
from sageintacctsdk.exceptions import InvalidTokenError
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

Fix incorrect DRF status import (will break module import).

status should be imported from rest_framework, not from rest_framework.views. As written, from rest_framework.views import status will raise an ImportError and prevent this module from loading.

Apply this change:

-from rest_framework import generics
-from rest_framework.exceptions import ValidationError
-from rest_framework.request import Request
-from rest_framework.response import Response
-from rest_framework.views import status
+from rest_framework import generics, status
+from rest_framework.exceptions import ValidationError
+from rest_framework.request import Request
+from rest_framework.response import Response
📝 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 jwt
import requests
from django.conf import settings
from django.core.cache import cache
from django.db.models import Q
from fyle_accounting_library.common_resources.enums import DimensionDetailSourceTypeEnum
from fyle_accounting_library.common_resources.models import DimensionDetail
from fyle_accounting_mappings.models import DestinationAttribute
from fyle_accounting_mappings.serializers import DestinationAttributeSerializer
from rest_framework import generics
from rest_framework.views import status
from rest_framework.exceptions import ValidationError
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.exceptions import ValidationError
from rest_framework.views import status
from sageintacctsdk.exceptions import InvalidTokenError
import jwt
import requests
from django.conf import settings
from django.core.cache import cache
from django.db.models import Q
from fyle_accounting_library.common_resources.enums import DimensionDetailSourceTypeEnum
from fyle_accounting_library.common_resources.models import DimensionDetail
from fyle_accounting_mappings.models import DestinationAttribute
from fyle_accounting_mappings.serializers import DestinationAttributeSerializer
from rest_framework import generics, status
from rest_framework.exceptions import ValidationError
from rest_framework.request import Request
from rest_framework.response import Response
from sageintacctsdk.exceptions import InvalidTokenError
🤖 Prompt for AI Agents
In apps/sage_intacct/views.py around lines 3 to 17, the code imports status from
rest_framework.views which is incorrect and will raise ImportError; replace that
import so status is imported from rest_framework (i.e., change the import line
to use "from rest_framework import status") and remove the incorrect
rest_framework.views import to ensure the module can load.

Comment on lines +3806 to +3829
CREATE TABLE public.sage_intacct_attributes_count (
id integer NOT NULL,
accounts_count integer NOT NULL,
items_count integer NOT NULL,
vendors_count integer NOT NULL,
employees_count integer NOT NULL,
departments_count integer NOT NULL,
classes_count integer NOT NULL,
customers_count integer NOT NULL,
projects_count integer NOT NULL,
locations_count integer NOT NULL,
expense_types_count integer NOT NULL,
tax_details_count integer NOT NULL,
cost_codes_count integer NOT NULL,
cost_types_count integer NOT NULL,
user_defined_dimensions_details jsonb,
charge_card_accounts_count integer NOT NULL,
payment_accounts_count integer NOT NULL,
expense_payment_types_count integer NOT NULL,
allocations_count integer NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
workspace_id integer 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.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify initialization of NOT NULL count columns in workspace creation.

All 18 count columns in sage_intacct_attributes_count are marked NOT NULL without default values. Per the AI summary, this table is initialized on workspace creation. Ensure the initialization logic (likely in a signal handler or workspace view) properly seeds all count fields to prevent INSERT failures.

Consider adding DEFAULT 0 to count columns if they're meant to start at zero, or verify the application initialization logic always populates these values:


🏁 Script executed:

#!/bin/bash
# Verify initialization logic for sage_intacct_attributes_count on workspace creation
# Search for workspace creation signals, views, or serializers that initialize this model

rg -n --type=py "SageIntacctAttributesCount\|sage_intacct_attributes_count" -A 3 -B 3 | head -100

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Broader search for sage_intacct references and workspace creation patterns

echo "=== Searching for sage_intacct references ==="
rg -n "sage_intacct" --type=py -C 2 | head -150

echo -e "\n=== Searching for workspace creation or signals ==="
rg -n "workspace.*create\|post_save.*workspace" --type=py -C 3 | head -150

echo -e "\n=== Looking for model files that might define the table ==="
fd "models" --type=f -e py | head -20

Length of output: 12119


🏁 Script executed:

#!/bin/bash
# Check sage_intacct models for SageIntacctAttributesCount definition

echo "=== Checking sage_intacct/models.py for attributes count model ==="
cat apps/sage_intacct/models.py | grep -n "SageIntacctAttributesCount\|attributes_count" -A 10 -B 2 | head -80

echo -e "\n=== Checking workspaces/models.py for workspace model ==="
cat apps/workspaces/models.py | grep -n "class Workspace\|def __init__\|post_save\|signals" -A 5 | head -100

echo -e "\n=== Searching for signals.py files ==="
fd "signals.py" --type=f

echo -e "\n=== Checking for initialization in migrations ==="
fd "migrations" --type=d | head -5

Length of output: 3251


🏁 Script executed:

#!/bin/bash
# Get complete model and check workspace signals

echo "=== Complete SageIntacctAttributesCount model definition ==="
sed -n '2002,2050p' apps/sage_intacct/models.py

echo -e "\n=== Workspaces signals.py for workspace creation logic ==="
cat apps/workspaces/signals.py

echo -e "\n=== Check recent migrations for sage_intacct_attributes_count ==="
fd "sage_intacct" --type=f -path "*migrations*" -e py | head -20 | xargs grep -l "attributes_count" 2>/dev/null | head -5

Length of output: 7009


Fix SQL schema: Add DEFAULT 0 to all 18 count columns in sage_intacct_attributes_count table.

The Python ORM model (apps/sage_intacct/models.py) defines all 18 count fields with default=0, but the SQL fixture has these fields marked NOT NULL without defaults. This mismatch will cause INSERT failures when using reset_db.sql for testing.

Add DEFAULT 0 to all integer count columns: accounts_count, items_count, vendors_count, employees_count, departments_count, classes_count, customers_count, projects_count, locations_count, expense_types_count, tax_details_count, cost_codes_count, cost_types_count, charge_card_accounts_count, payment_accounts_count, expense_payment_types_count, and allocations_count.

🤖 Prompt for AI Agents
In tests/sql_fixtures/reset_db_fixtures/reset_db.sql around lines 3806-3829, the
18 integer "count" columns in the CREATE TABLE for
public.sage_intacct_attributes_count are NOT NULL but lack defaults; update the
CREATE TABLE to add DEFAULT 0 to each of the count columns (accounts_count,
items_count, vendors_count, employees_count, departments_count, classes_count,
customers_count, projects_count, locations_count, expense_types_count,
tax_details_count, cost_codes_count, cost_types_count,
charge_card_accounts_count, payment_accounts_count, expense_payment_types_count,
allocations_count) while keeping NOT NULL so the SQL fixture matches the Python
ORM defaults and avoids INSERT failures during tests.

@github-actions github-actions bot added size/XL Extra Large PR and removed size/L Large PR labels Nov 21, 2025
@github-actions
Copy link

Coverage

Coverage Report
FileStmtsMissCoverMissing
apps
   exceptions.py471177%27, 33, 39–44, 50, 56–61, 67, 73, 79–80
apps/fyle
   actions.py124794%214, 263, 319–323
   constants.py10100% 
   helpers.py2352291%50–57, 66, 146–162, 376, 470, 477–480, 483–484
   models.py3362194%313–315, 319–321, 325–327, 331–333, 337–339, 343–345, 406, 516–537, 568, 585
   queue.py470100% 
   serializers.py410100% 
   signals.py35683%22, 55–56, 69–71
   tasks.py4487384%154–155, 158–159, 162–163, 185–186, 220–230, 242–247, 290–293, 314–318, 328–336, 371–372, 402–449, 733–737
   views.py1721591%84–91, 145, 245–246, 290–291, 316–320, 413–417
apps/internal
   actions.py39295%26–27
   helpers.py361364%18–35
   serializers.py300100% 
   views.py97199%39
apps/internal/services
   e2e_setup.py590100% 
   fixture_factory.py59395%218–220
apps/mappings
   constants.py20100% 
   exceptions.py66395%69–71
   helpers.py300100% 
   models.py500100% 
   schedules.py150100% 
   serializers.py100100% 
   signals.py1071289%84–86, 121, 174–179, 185–189, 195–198
   tasks.py1682685%105–106, 190–207, 237, 240, 243, 249, 252, 258, 297–299, 310, 382
   utils.py340100% 
   views.py46491%34–35, 114–115
apps/sage_intacct
   actions.py29486%21–22, 49–50
   dependent_fields.py2704085%315, 351–354, 383–392, 394–395, 397–399, 401–402, 404–405, 409–411, 455, 475, 515–517, 563–565, 613–619, 623–624
   helpers.py40295%51–52
   models.py9008491%58–59, 209, 316, 323, 367, 388–405, 464, 476, 526, 532, 567, 617–619, 633–646, 657, 693, 812, 851, 1047, 1138–1141, 1211, 1312–1332, 1410, 1426, 1834–1888, 1919, 1963–1999
   queue.py1631690%45, 53, 58, 105, 127–130, 170, 184, 199–202, 237, 258–261, 296, 318–321
   serializers.py350100% 
   tasks.py103913687%77–80, 114, 150–151, 155–158, 160, 163, 213, 247–248, 394, 400, 408, 572–573, 593–594, 640–641, 645, 664, 679, 684, 689, 696, 702, 716, 753–754, 821–822, 826, 847, 862, 868, 871–874, 881, 895–899, 936–937, 993–994, 996, 1017, 1032, 1038, 1041–1044, 1051, 1065–1069, 1105–1106, 1162–1163, 1166, 1187, 1202, 1208, 1214, 1221, 1227, 1241, 1293–1294, 1360–1369, 1411–1435, 1554–1558, 1614–1620, 1706–1716, 1719, 1728–1729, 1763–1765, 1799–1802, 1870–1874, 1883
   utils.py8764895%96, 108, 111, 536–537, 714–718, 859, 864, 875, 1165, 1181–1187, 1196–1197, 1672–1682, 1910–1914, 1942–1946, 1974–1978, 2020–2024, 2077–2081, 2101, 2110, 2119, 2128–2129, 2140, 2163–2165
   views.py1433377%126–128, 133–134, 140–141, 184–187, 256–286, 297–312, 325–329
apps/sage_intacct/errors
   errors.py20100% 
   helpers.py26196%73
apps/tasks
   models.py680100% 
   serializers.py60100% 
   views.py370100% 
apps/users
   helpers.py120100% 
   models.py530100% 
   views.py170100% 
apps/workspaces
   actions.py46980%65, 84–85, 103, 122–133, 147
   enums.py50100% 
   helpers.py80100% 
   models.py175498%338–340, 350–352
   permissions.py34974%33, 59–67
   serializers.py39197%53
   signals.py310100% 
   tasks.py154895%52–58, 186–193, 234, 266–267
   utils.py90100% 
   views.py262698%115–118, 484–485
apps/workspaces/apis/advanced_settings
   serializers.py80396%260, 263, 266
   triggers.py100100% 
   views.py110100% 
apps/workspaces/apis/errors
   serializers.py200100% 
   views.py150100% 
apps/workspaces/apis/export_settings
   helpers.py730100% 
   serializers.py103397%267, 270, 273
   triggers.py39295%32–33
   views.py110100% 
apps/workspaces/apis/import_settings
   serializers.py1411887%232–238, 243–249, 257–265, 283, 286, 314, 317–318, 326
   triggers.py47198%31
   views.py330100% 
fyle_integrations_imports
   dataclasses.py310100% 
   models.py230100% 
   queues.py34585%24, 99, 136, 148–149
   signals.py100100% 
   tasks.py926628%66–103, 112–118, 131–194, 198–213
fyle_integrations_imports/modules
   base.py182796%75, 91–92, 95, 224–225, 343
   categories.py1712983%79, 95, 98–99, 102–103, 270–271, 281, 294, 296, 340, 357–403
   cost_centers.py90891%131–132, 150–151, 160, 176, 178, 224
   expense_custom_fields.py121794%83–88, 247, 263, 265, 294
   merchants.py1002179%79–82, 110–128, 154–161, 177–178, 188, 201, 203, 233
   projects.py82594%107–108, 118, 142, 189
   tax_groups.py180100% 
   webhook_attributes.py127695%95, 97–99, 208–209
workers
   actions.py210100% 
   helpers.py410100% 
   worker.py56591%44–45, 77–78, 125
TOTAL859580691% 

Tests Skipped Failures Errors Time
488 0 💤 0 ❌ 0 🔥 42.423s ⏱️

@github-actions
Copy link


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

apps/sage_intacct/models.py (100%)
apps/sage_intacct/serializers.py (100%)
apps/sage_intacct/utils.py (98.8%): Missing lines 859
apps/sage_intacct/views.py (100%)
apps/workspaces/views.py (100%)

Total: 143 lines
Missing: 1 line
Coverage: 99%

@github-actions
Copy link

Coverage

Coverage Report
FileStmtsMissCoverMissing
apps
   exceptions.py471177%27, 33, 39–44, 50, 56–61, 67, 73, 79–80
apps/fyle
   actions.py124794%214, 263, 319–323
   constants.py10100% 
   helpers.py2352291%50–57, 66, 146–162, 376, 470, 477–480, 483–484
   models.py3362194%313–315, 319–321, 325–327, 331–333, 337–339, 343–345, 406, 516–537, 568, 585
   queue.py470100% 
   serializers.py410100% 
   signals.py35683%22, 55–56, 69–71
   tasks.py4487384%154–155, 158–159, 162–163, 185–186, 220–230, 242–247, 290–293, 314–318, 328–336, 371–372, 402–449, 733–737
   views.py1721591%84–91, 145, 245–246, 290–291, 316–320, 413–417
apps/internal
   actions.py39295%26–27
   helpers.py361364%18–35
   serializers.py300100% 
   views.py97199%39
apps/internal/services
   e2e_setup.py590100% 
   fixture_factory.py59395%218–220
apps/mappings
   constants.py20100% 
   exceptions.py66395%69–71
   helpers.py300100% 
   models.py500100% 
   schedules.py150100% 
   serializers.py100100% 
   signals.py1071289%84–86, 121, 174–179, 185–189, 195–198
   tasks.py1682685%105–106, 190–207, 237, 240, 243, 249, 252, 258, 297–299, 310, 382
   utils.py340100% 
   views.py46491%34–35, 114–115
apps/sage_intacct
   actions.py29486%21–22, 49–50
   dependent_fields.py2704085%315, 351–354, 383–392, 394–395, 397–399, 401–402, 404–405, 409–411, 455, 475, 515–517, 563–565, 613–619, 623–624
   helpers.py40295%51–52
   models.py9008491%58–59, 209, 316, 323, 367, 388–405, 464, 476, 526, 532, 567, 617–619, 633–646, 657, 693, 812, 851, 1047, 1138–1141, 1211, 1312–1332, 1410, 1426, 1834–1888, 1919, 1963–1999
   queue.py1631690%45, 53, 58, 105, 127–130, 170, 184, 199–202, 237, 258–261, 296, 318–321
   serializers.py350100% 
   tasks.py103913687%77–80, 114, 150–151, 155–158, 160, 163, 213, 247–248, 394, 400, 408, 572–573, 593–594, 640–641, 645, 664, 679, 684, 689, 696, 702, 716, 753–754, 821–822, 826, 847, 862, 868, 871–874, 881, 895–899, 936–937, 993–994, 996, 1017, 1032, 1038, 1041–1044, 1051, 1065–1069, 1105–1106, 1162–1163, 1166, 1187, 1202, 1208, 1214, 1221, 1227, 1241, 1293–1294, 1360–1369, 1411–1435, 1554–1558, 1614–1620, 1706–1716, 1719, 1728–1729, 1763–1765, 1799–1802, 1870–1874, 1883
   utils.py8764895%96, 108, 111, 536–537, 714–718, 859, 864, 875, 1165, 1181–1187, 1196–1197, 1672–1682, 1910–1914, 1942–1946, 1974–1978, 2020–2024, 2077–2081, 2101, 2110, 2119, 2128–2129, 2140, 2163–2165
   views.py1433377%126–128, 133–134, 140–141, 184–187, 256–286, 297–312, 325–329
apps/sage_intacct/errors
   errors.py20100% 
   helpers.py26196%73
apps/tasks
   models.py680100% 
   serializers.py60100% 
   views.py370100% 
apps/users
   helpers.py120100% 
   models.py530100% 
   views.py170100% 
apps/workspaces
   actions.py46980%65, 84–85, 103, 122–133, 147
   enums.py50100% 
   helpers.py80100% 
   models.py175498%338–340, 350–352
   permissions.py34974%33, 59–67
   serializers.py39197%53
   signals.py310100% 
   tasks.py154895%52–58, 186–193, 234, 266–267
   utils.py90100% 
   views.py262698%115–118, 484–485
apps/workspaces/apis/advanced_settings
   serializers.py80396%260, 263, 266
   triggers.py100100% 
   views.py110100% 
apps/workspaces/apis/errors
   serializers.py200100% 
   views.py150100% 
apps/workspaces/apis/export_settings
   helpers.py730100% 
   serializers.py103397%267, 270, 273
   triggers.py39295%32–33
   views.py110100% 
apps/workspaces/apis/import_settings
   serializers.py1411887%232–238, 243–249, 257–265, 283, 286, 314, 317–318, 326
   triggers.py47198%31
   views.py330100% 
fyle_integrations_imports
   dataclasses.py310100% 
   models.py230100% 
   queues.py34585%24, 99, 136, 148–149
   signals.py100100% 
   tasks.py926628%66–103, 112–118, 131–194, 198–213
fyle_integrations_imports/modules
   base.py182796%75, 91–92, 95, 224–225, 343
   categories.py1712983%79, 95, 98–99, 102–103, 270–271, 281, 294, 296, 340, 357–403
   cost_centers.py90891%131–132, 150–151, 160, 176, 178, 224
   expense_custom_fields.py121794%83–88, 247, 263, 265, 294
   merchants.py1002179%79–82, 110–128, 154–161, 177–178, 188, 201, 203, 233
   projects.py82594%107–108, 118, 142, 189
   tax_groups.py180100% 
   webhook_attributes.py127695%95, 97–99, 208–209
workers
   actions.py210100% 
   helpers.py410100% 
   worker.py56591%44–45, 77–78, 125
TOTAL859580691% 

Tests Skipped Failures Errors Time
488 0 💤 0 ❌ 0 🔥 45.182s ⏱️

@github-actions
Copy link


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

apps/sage_intacct/models.py (100%)
apps/sage_intacct/serializers.py (100%)
apps/sage_intacct/utils.py (98.8%): Missing lines 859
apps/sage_intacct/views.py (100%)
apps/workspaces/views.py (100%)

Total: 143 lines
Missing: 1 line
Coverage: 99%

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

🧹 Nitpick comments (9)
apps/internal/migrations/0026_auto_generated_sql.py (1)

20-27: Reverse SQL effectively truncates sage_intacct_attributes_count

The reverse_sql condition:

DELETE FROM sage_intacct_attributes_count
WHERE EXISTS (
    SELECT 1
    FROM sage_intacct_attributes_count
    WHERE workspace_id = sage_intacct_attributes_count.workspace_id
);

is effectively true for every row as soon as the table is non‑empty (the subquery is uncorrelated and workspace_id = workspace_id is tautological), so this acts like a full table delete, not just a rollback of rows inserted by the forward migration.

If you intend a full truncate on downgrade, consider making that explicit:

DELETE FROM sage_intacct_attributes_count;

If instead you only want to remove rows created by this migration, you’ll likely need a stricter predicate (e.g., on timestamps or matching to workspaces) or accept that a precise reverse isn’t feasible and drop reverse_sql entirely.

apps/sage_intacct/utils.py (1)

906-954: Persisting UDD dimension counts into user_defined_dimensions_details

The udd_counts aggregation and final get_or_create on SageIntacctAttributesCount to save user_defined_dimensions_details is a good fit for the JSON field and keeps per‑dimension counts even when a particular UDD is over the sync limit and skipped.

One minor consideration: if dimensions is large and sync_user_defined_dimensions is called often, you might eventually want to narrow the update to only changed keys or batch updates, but for current scope this is fine.

tests/test_workspaces/test_views.py (1)

13-20: Workspace creation test correctly asserts attributes-count initialization

The new import and test_workspace_creation_creates_attributes_count nicely verify that posting to /api/workspaces/ results in a SageIntacctAttributesCount row for the returned workspace_id, with initial accounts_count, vendors_count, and employees_count all zero.

If you ever need stricter guarantees, you could additionally assert that this record did not exist before the POST, but for now this gives good coverage of the intended behavior.

Also applies to: 213-230

tests/test_sageintacct/test_views.py (1)

5-5: End‑to‑end coverage for attributes_count endpoint

The new test_sage_intacct_attributes_count_view:

  • Seeds SageIntacctAttributesCount for workspace_id=1.
  • Asserts the API returns the expected counts and omits user_defined_dimensions_details while including workspace.
  • Confirms a 404 after deleting the record.

This is solid coverage of both view and serializer behavior. The extra db fixture argument is unused and could be dropped if you want to silence ARG001, but it’s not harmful.

Also applies to: 195-220

tests/test_sageintacct/test_models.py (1)

1394-1412: Model test validates SageIntacctAttributesCount.update_attribute_count behavior

test_sage_intacct_attributes_count_model correctly:

  • Starts from a clean slate for workspace_id=1.
  • Creates a record via the workspace relation, asserting default counts are zero.
  • Calls update_attribute_count for accounts and vendors and refreshes from DB to confirm the fields change as expected, including overwrite behavior.

This is a good, focused unit test for the model’s update helper. Again, db isn’t used directly in the body, but its presence is standard for DB‑using tests.

tests/test_sageintacct/test_utils.py (4)

2022-2041: Use timezone-aware datetimes for consistency.

The test uses naive datetime objects (lines 2031, 2036) when updating workspace.created_at, but the is_sync_allowed implementation compares against timezone-aware datetimes. While Django typically handles this, it's better to be explicit for consistency.

Apply this diff:

 def test_is_sync_allowed_standard_attributes(db):
     """
     Test is sync allowed for standard attributes
     """
     workspace_id = 1
     intacct_credentials = SageIntacctCredential.objects.get(workspace_id=workspace_id)
     sage_intacct_connection = SageIntacctConnector(credentials_object=intacct_credentials, workspace_id=workspace_id)
     is_allowed = sage_intacct_connection.is_sync_allowed(attribute_count=5000)
     assert is_allowed is True
-    old_date = datetime(2024, 9, 1)
+    old_date = timezone.make_aware(datetime(2024, 9, 1))
     Workspace.objects.filter(id=workspace_id).update(created_at=old_date)
     sage_intacct_connection = SageIntacctConnector(credentials_object=intacct_credentials, workspace_id=workspace_id)
     is_allowed = sage_intacct_connection.is_sync_allowed(attribute_count=35000)
     assert is_allowed is True
-    new_date = datetime(2024, 10, 15)
+    new_date = timezone.make_aware(datetime(2024, 10, 15))
     Workspace.objects.filter(id=workspace_id).update(created_at=new_date)
     sage_intacct_connection = SageIntacctConnector(credentials_object=intacct_credentials, workspace_id=workspace_id)
     is_allowed = sage_intacct_connection.is_sync_allowed(attribute_count=35000)
     assert is_allowed is False

2043-2062: Use timezone-aware datetimes for consistency.

Same issue as the previous test: naive datetime objects are used when updating workspace.created_at. Use timezone.make_aware() for consistency with the implementation.

Apply this diff:

-    old_date = datetime(2024, 9, 1)
+    old_date = timezone.make_aware(datetime(2024, 9, 1))
     Workspace.objects.filter(id=workspace_id).update(created_at=old_date)
     sage_intacct_connection = SageIntacctConnector(credentials_object=intacct_credentials, workspace_id=workspace_id)
     is_allowed = sage_intacct_connection.is_sync_allowed(attribute_count=600000, attribute_type='cost_types')
     assert is_allowed is True
-    new_date = datetime(2024, 10, 15)
+    new_date = timezone.make_aware(datetime(2024, 10, 15))
     Workspace.objects.filter(id=workspace_id).update(created_at=new_date)

2064-2103: Fix timezone inconsistency and unused loop variable.

Two minor issues:

  1. Line 2069 uses a naive datetime object (same issue as previous tests)
  2. Line 2085: The loop variable name is not used within the loop body

Apply this diff:

     workspace_id = 1
-    Workspace.objects.filter(id=workspace_id).update(created_at=datetime(2024, 10, 15))
+    Workspace.objects.filter(id=workspace_id).update(created_at=timezone.make_aware(datetime(2024, 10, 15)))
     intacct_credentials = SageIntacctCredential.objects.get(workspace_id=workspace_id)
     sage_intacct_connection = SageIntacctConnector(credentials_object=intacct_credentials, workspace_id=workspace_id)
     from fyle_accounting_mappings.models import DestinationAttribute
 
     from apps.sage_intacct.models import CostCode
     sync_tests = [
         ('Vendors', 'sageintacctsdk.apis.Vendors.count', sage_intacct_connection.sync_vendors, 'VENDOR', DestinationAttribute),
         ('Employees', 'sageintacctsdk.apis.Employees.count', sage_intacct_connection.sync_employees, 'EMPLOYEE', DestinationAttribute),
         ('Items', 'sageintacctsdk.apis.Items.count', sage_intacct_connection.sync_items, 'ITEM', DestinationAttribute),
         ('ChargeCardAccounts', 'sageintacctsdk.apis.ChargeCardAccounts.count', sage_intacct_connection.sync_charge_card_accounts, 'CHARGE_CARD_NUMBER', DestinationAttribute),
         ('CheckingAccounts', 'sageintacctsdk.apis.CheckingAccounts.count', sage_intacct_connection.sync_payment_accounts, 'PAYMENT_ACCOUNT', DestinationAttribute),
         ('Tasks', 'sageintacctsdk.apis.Tasks.count', sage_intacct_connection.sync_cost_codes, None, CostCode),
         ('ExpensePaymentTypes', 'sageintacctsdk.apis.ExpensePaymentTypes.count', sage_intacct_connection.sync_expense_payment_types, 'EXPENSE_PAYMENT_TYPE', DestinationAttribute),
         ('Allocations', 'sageintacctsdk.apis.Allocations.count', sage_intacct_connection.sync_allocations, 'ALLOCATION', DestinationAttribute),
     ]
-    for name, mock_path, sync_method, attr_type, model in sync_tests:
+    for _name, mock_path, sync_method, attr_type, model in sync_tests:
         mocker.patch(mock_path, return_value=35000)

2132-2225: Consider refactoring to reduce test function length and duplication.

This test function has 91 statements, exceeding the recommended limit of 50. The highly repetitive pattern (mock, sync, assert count) could be refactored into a parameterized test or helper function for better maintainability.

Consider using pytest.mark.parametrize to reduce duplication:

@pytest.mark.parametrize("api_path,count_value,sync_method,count_field,extra_mocks", [
    ('sageintacctsdk.apis.Accounts.count', 2500, 'sync_accounts', 'accounts_count', ['get_all_generator', 'platform_connector']),
    ('sageintacctsdk.apis.Items.count', 4500, 'sync_items', 'items_count', ['get_all_generator']),
    ('sageintacctsdk.apis.Vendors.count', 8000, 'sync_vendors', 'vendors_count', ['get_all_generator']),
    # ... etc
])
def test_sync_method_persists_count(mocker, db, api_path, count_value, sync_method, count_field, extra_mocks):
    workspace_id = 1
    SageIntacctAttributesCount.objects.filter(workspace_id=workspace_id).delete()
    
    # Setup mocks
    mocker.patch(api_path, return_value=count_value)
    # Setup extra mocks based on extra_mocks list
    
    # Execute sync
    intacct_credentials = SageIntacctCredential.objects.get(workspace_id=workspace_id)
    sage_intacct_connection = SageIntacctConnector(credentials_object=intacct_credentials, workspace_id=workspace_id)
    getattr(sage_intacct_connection, sync_method)()
    
    # Verify count
    count_record = SageIntacctAttributesCount.objects.get(workspace_id=workspace_id)
    assert getattr(count_record, count_field) == count_value

Alternatively, keep as a single comprehensive test but extract the repetitive assertion pattern into a helper function.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae21b4c and 7253708.

📒 Files selected for processing (7)
  • apps/internal/migrations/0026_auto_generated_sql.py (1 hunks)
  • apps/sage_intacct/utils.py (23 hunks)
  • tests/test_mappings/test_tasks.py (3 hunks)
  • tests/test_sageintacct/test_models.py (2 hunks)
  • tests/test_sageintacct/test_utils.py (10 hunks)
  • tests/test_sageintacct/test_views.py (2 hunks)
  • tests/test_workspaces/test_views.py (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
tests/test_workspaces/test_views.py (2)
apps/sage_intacct/models.py (1)
  • SageIntacctAttributesCount (2002-2050)
apps/workspaces/views.py (4)
  • post (103-143)
  • post (210-273)
  • post (324-411)
  • post (584-608)
tests/test_sageintacct/test_models.py (2)
apps/sage_intacct/models.py (2)
  • SageIntacctAttributesCount (2002-2050)
  • update_attribute_count (2038-2050)
apps/workspaces/models.py (1)
  • Workspace (83-105)
tests/test_sageintacct/test_utils.py (3)
apps/sage_intacct/models.py (1)
  • SageIntacctAttributesCount (2002-2050)
apps/workspaces/models.py (2)
  • Workspace (83-105)
  • IntacctSyncedTimestamp (304-357)
apps/sage_intacct/utils.py (1)
  • is_sync_allowed (179-195)
apps/sage_intacct/utils.py (2)
apps/sage_intacct/models.py (2)
  • SageIntacctAttributesCount (2002-2050)
  • update_attribute_count (2038-2050)
apps/workspaces/models.py (1)
  • Workspace (83-105)
tests/test_sageintacct/test_views.py (2)
apps/sage_intacct/models.py (1)
  • SageIntacctAttributesCount (2002-2050)
tests/conftest.py (2)
  • api_client (17-21)
  • test_connection (67-102)
🪛 Pylint (4.0.3)
apps/internal/migrations/0026_auto_generated_sql.py

[error] 1-1: Unrecognized option found: suggestion-mode, no-space-check

(E0015)


[refactor] 1-1: Useless option value for '--disable', 'print-statement' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'parameter-unpacking' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'unpacking-in-except' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'old-raise-syntax' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'backtick' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'import-star-module-level' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'apply-builtin' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'basestring-builtin' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'buffer-builtin' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'cmp-builtin' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'coerce-builtin' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'execfile-builtin' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'file-builtin' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'long-builtin' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'raw_input-builtin' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'reduce-builtin' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'standarderror-builtin' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'unicode-builtin' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'xrange-builtin' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'coerce-method' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'delslice-method' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'getslice-method' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'setslice-method' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'no-absolute-import' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'old-division' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'dict-iter-method' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'dict-view-method' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'next-method-called' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'metaclass-assignment' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'indexing-exception' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'raising-string' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'reload-builtin' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'oct-method' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'hex-method' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'nonzero-method' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'cmp-method' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'input-builtin' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'round-builtin' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'intern-builtin' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'unichr-builtin' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'map-builtin-not-iterating' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'zip-builtin-not-iterating' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'range-builtin-not-iterating' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'filter-builtin-not-iterating' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'using-cmp-argument' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'div-method' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'idiv-method' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'rdiv-method' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'exception-message-attribute' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'invalid-str-codec' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'sys-max-int' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'bad-python3-import' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'deprecated-string-function' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'deprecated-str-translate-call' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'deprecated-itertools-function' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'deprecated-types-field' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'next-method-defined' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'dict-items-not-iterating' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'dict-keys-not-iterating' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'dict-values-not-iterating' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'deprecated-operator-function' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'deprecated-urllib-function' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'xreadlines-attribute' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'deprecated-sys-function' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'exception-escape' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'comprehension-escape' was removed from pylint, see pylint-dev/pylint#4942.

(R0022)


[refactor] 1-1: Useless option value for '--disable', 'no-self-use' was moved to an optional extension, see https://pylint.readthedocs.io/en/latest/whatsnew/2/2.14/summary.html#removed-checkers.

(R0022)

tests/test_sageintacct/test_utils.py

[refactor] 2132-2132: Too many statements (91/50)

(R0915)

🪛 Ruff (0.14.5)
apps/internal/migrations/0026_auto_generated_sql.py

6-6: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


8-29: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

tests/test_sageintacct/test_models.py

1394-1394: Unused function argument: db

(ARG001)

tests/test_sageintacct/test_utils.py

2022-2022: Unused function argument: db

(ARG001)


2043-2043: Unused function argument: db

(ARG001)


2064-2064: Unused function argument: db

(ARG001)


2085-2085: Loop control variable name not used within loop body

Rename unused name to _name

(B007)


2105-2105: Unused function argument: db

(ARG001)


2118-2118: Unused function argument: field

(ARG001)


2118-2118: Unused function argument: value

(ARG001)


2118-2118: Unused function argument: updated_at

(ARG001)


2121-2121: Unused function argument: field

(ARG001)


2132-2132: Unused function argument: db

(ARG001)


2132-2132: Unused function argument: create_dependent_field_setting

(ARG001)

apps/sage_intacct/utils.py

179-179: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)

tests/test_sageintacct/test_views.py

195-195: Unused function argument: db

(ARG001)

🔇 Additional comments (9)
tests/test_mappings/test_tasks.py (2)

241-257: Covering NoPrivilegeError from both Employees.count and get_all_generator

Adding the Employees.count mock ensures the new count-based pre-check path also surfaces the expected log message; this keeps the test aligned with the updated implementation that calls count() before iterating. Looks good.


323-347: New .count() mocks for Vendors and Allocations keep sync test aligned with implementation

The additional Vendors.count and Allocations.count mocks are consistent with the existing get_all_generator mocks and ensure sync_sage_intacct_attributes can exercise the new count-based gating without unexpected real SDK calls. No issues from a test-behavior standpoint.

apps/sage_intacct/utils.py (2)

228-239: Pattern of counting, persisting, then gating attribute syncs looks consistent

Across sync_accounts, sync_departments, sync_expense_types, sync_charge_card_accounts, sync_payment_accounts, sync_cost_types, sync_cost_codes, sync_projects, sync_items, sync_locations, sync_expense_payment_types, sync_employees, sync_allocations, sync_classes, sync_customers, sync_tax_details, and sync_vendors:

  • Each method calls the relevant .count() on the SDK, persists to SageIntacctAttributesCount.update_attribute_count(...) with an attribute_type that matches the {attribute_type}_count field on the model, then applies is_sync_allowed before doing any heavier work.
  • Logging messages clearly mention the workspace, the count, and the applied limit.

This pattern is coherent and should give you reliable per-workspace telemetry with a cheap pre‑flight gate. I don’t see functional issues in this sequence.

Also applies to: 285-300, 340-351, 404-417, 441-452, 479-490, 516-528, 551-562, 608-619, 660-670, 757-768, 798-809, 844-855, 967-977, 1011-1022, 1056-1067, 1286-1297


34-37: No signature-change risk found — all call sites use keyword arguments

Verification across the codebase shows all invocations of is_sync_allowed use keyword arguments, eliminating the risk of positional argument swapping. Additionally, connector.py defines a separate private method __is_sync_allowed in a different class (SageIntacctDimensionSyncManager), so there is no cross-module collision.

The suggestion to make parameters keyword-only remains a valid optional refactoring for defensiveness, but it is not critical given the current safe usage patterns.

tests/test_sageintacct/test_models.py (1)

4-41: Expanded imports reflect new Sage Intacct models and helpers

The broader import list (including SageIntacctAttributesCount, Workspace, Configuration, SageIntacctCredential, and helper functions like get_allocation_id_or_none, get_or_create_credit_card_vendor) matches the usages throughout this test module. No obvious unused additions within the shown code.

tests/test_sageintacct/test_utils.py (4)

6-6: LGTM! Imports support new count tracking tests.

The added imports for timezone and SageIntacctAttributesCount are necessary for the new test functions that validate sync allowance logic and count persistence.

Also applies to: 11-11


27-30: LGTM! Count mocks align with new tracking functionality.

The added mocks for various count() API methods are consistent with the PR's objective to track and persist attribute counts in the SageIntacctAttributesCount model.

Also applies to: 78-81, 161-164, 187-190, 319-322, 490-493


1522-1562: LGTM! Updated mock values correctly test sync limit enforcement.

The mock return values are set to 30001 to properly test that syncs are skipped when attribute counts exceed the SYNC_UPPER_LIMIT for new workspaces.


2105-2130: LGTM! Mock function signatures correctly match expected interface.

The unused parameters in the mock functions (lines 2118, 2121) are intentional to match the expected function signatures and can be safely ignored.

@ashwin1111 ashwin1111 requested review from Ashutosh619-sudo and Hrishabh17 and removed request for ashwin1111 November 24, 2025 07:01
@github-actions
Copy link

Coverage

Coverage Report
FileStmtsMissCoverMissing
apps
   exceptions.py471177%27, 33, 39–44, 50, 56–61, 67, 73, 79–80
apps/fyle
   actions.py124794%214, 263, 319–323
   constants.py10100% 
   helpers.py2352291%50–57, 66, 146–162, 376, 470, 477–480, 483–484
   models.py3362194%313–315, 319–321, 325–327, 331–333, 337–339, 343–345, 406, 516–537, 568, 585
   queue.py470100% 
   serializers.py410100% 
   signals.py35683%22, 55–56, 69–71
   tasks.py4487384%154–155, 158–159, 162–163, 185–186, 220–230, 242–247, 290–293, 314–318, 328–336, 371–372, 402–449, 733–737
   views.py1721591%84–91, 145, 245–246, 290–291, 316–320, 413–417
apps/internal
   actions.py39295%26–27
   helpers.py361364%18–35
   serializers.py300100% 
   views.py97199%39
apps/internal/services
   e2e_setup.py590100% 
   fixture_factory.py59395%218–220
apps/mappings
   constants.py20100% 
   exceptions.py66395%69–71
   helpers.py300100% 
   models.py500100% 
   schedules.py150100% 
   serializers.py100100% 
   signals.py1071289%84–86, 121, 174–179, 185–189, 195–198
   tasks.py1683380%105–106, 190–207, 225, 228, 234, 237, 240, 243, 248–258, 297–299, 310, 382
   utils.py340100% 
   views.py46491%34–35, 114–115
apps/sage_intacct
   actions.py29486%21–22, 49–50
   dependent_fields.py2704085%315, 351–354, 383–392, 394–395, 397–399, 401–402, 404–405, 409–411, 455, 475, 515–517, 563–565, 613–619, 623–624
   helpers.py40295%51–52
   models.py9009989%58–59, 209, 316, 323, 367, 388–405, 464, 476, 526, 532, 567, 617–619, 633–646, 657, 693, 812, 851, 1047, 1138–1141, 1211, 1312–1332, 1410, 1426, 1834–1888, 1918–1954, 1963–1999, 2048–2050
   queue.py1631690%45, 53, 58, 105, 127–130, 170, 184, 199–202, 237, 258–261, 296, 318–321
   serializers.py350100% 
   tasks.py103913687%77–80, 114, 150–151, 155–158, 160, 163, 213, 247–248, 394, 400, 408, 572–573, 593–594, 640–641, 645, 664, 679, 684, 689, 696, 702, 716, 753–754, 821–822, 826, 847, 862, 868, 871–874, 881, 895–899, 936–937, 993–994, 996, 1017, 1032, 1038, 1041–1044, 1051, 1065–1069, 1105–1106, 1162–1163, 1166, 1187, 1202, 1208, 1214, 1221, 1227, 1241, 1293–1294, 1360–1369, 1411–1435, 1554–1558, 1614–1620, 1706–1716, 1719, 1728–1729, 1763–1765, 1799–1802, 1870–1874, 1883
   utils.py87631864%89–96, 104–121, 129–138, 209, 218–226, 240–283, 297–334, 348–398, 414–435, 449–473, 481–510, 525–545, 559–602, 616–654, 668–697, 714–718, 765–792, 806–838, 852–898, 928–930, 975–1005, 1019–1050, 1064–1095, 1165, 1181–1187, 1196–1197, 1294–1328, 1672–1682, 1910–1914, 1942–1946, 1974–1978, 2020–2024, 2077–2081, 2101, 2110, 2119, 2128–2129, 2140, 2163–2165
   views.py1433377%126–128, 133–134, 140–141, 184–187, 256–286, 297–312, 325–329
apps/sage_intacct/errors
   errors.py20100% 
   helpers.py26196%73
apps/tasks
   models.py680100% 
   serializers.py60100% 
   views.py370100% 
apps/users
   helpers.py120100% 
   models.py530100% 
   views.py170100% 
apps/workspaces
   actions.py46980%65, 84–85, 103, 122–133, 147
   enums.py50100% 
   helpers.py80100% 
   models.py175498%338–340, 350–352
   permissions.py34974%33, 59–67
   serializers.py39197%53
   signals.py310100% 
   tasks.py154895%52–58, 186–193, 234, 266–267
   utils.py90100% 
   views.py262698%115–118, 484–485
apps/workspaces/apis/advanced_settings
   serializers.py80396%260, 263, 266
   triggers.py100100% 
   views.py110100% 
apps/workspaces/apis/errors
   serializers.py200100% 
   views.py150100% 
apps/workspaces/apis/export_settings
   helpers.py730100% 
   serializers.py103397%267, 270, 273
   triggers.py39295%32–33
   views.py110100% 
apps/workspaces/apis/import_settings
   serializers.py1411887%232–238, 243–249, 257–265, 283, 286, 314, 317–318, 326
   triggers.py47198%31
   views.py330100% 
fyle_integrations_imports
   dataclasses.py310100% 
   models.py230100% 
   queues.py34585%24, 99, 136, 148–149
   signals.py100100% 
   tasks.py926628%66–103, 112–118, 131–194, 198–213
fyle_integrations_imports/modules
   base.py1825470%75, 91–92, 95, 171–181, 187–196, 224–225, 249–286, 296–302, 314–316, 342–345
   categories.py1716960%79, 86, 91–106, 118–151, 161–167, 216–227, 270–271, 281, 294, 296, 340, 357–403
   cost_centers.py90891%131–132, 150–151, 160, 176, 178, 224
   expense_custom_fields.py1211191%83–88, 218–226, 247, 263, 265, 294
   merchants.py1002179%79–82, 110–128, 154–161, 177–178, 188, 201, 203, 233
   projects.py82693%107–108, 118, 140, 142, 189
   tax_groups.py180100% 
   webhook_attributes.py127695%95, 97–99, 208–209
workers
   actions.py210100% 
   helpers.py410100% 
   worker.py56591%44–45, 77–78, 125
TOTAL8595119086% 

Tests Skipped Failures Errors Time
488 0 💤 30 ❌ 0 🔥 45.670s ⏱️

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 7253708 and 12b68f6.

📒 Files selected for processing (1)
  • apps/sage_intacct/models.py (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/sage_intacct/models.py (1)
apps/workspaces/models.py (9)
  • Workspace (83-105)
  • Meta (104-105)
  • Meta (168-169)
  • Meta (189-190)
  • Meta (213-214)
  • Meta (234-235)
  • Meta (256-257)
  • Meta (300-301)
  • Meta (328-329)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: pytest
🔇 Additional comments (2)
apps/sage_intacct/models.py (2)

5-23: LGTM! Import changes support the new model.

The additional imports align with the new SageIntacctAttributesCount model and related functionality.


2002-2035: Well-structured model with appropriate defaults.

The model structure is clean with:

  • Proper OneToOne relationship to Workspace with a descriptive related_name
  • Consistent field naming pattern across all count fields
  • Appropriate defaults (0 for counts, dict for JSONField)
  • Indexed updated_at field for query performance

Comment on lines +2037 to +2050
@staticmethod
def update_attribute_count(workspace_id: int, attribute_type: str, count: int) -> None:
"""
Update attribute count for a workspace
:param workspace_id: Workspace ID
:param attribute_type: Type of attribute (e.g., 'accounts', 'vendors')
:param count: Count value from Sage Intacct
"""
sage_intacct_count, _ = SageIntacctAttributesCount.objects.get(
workspace_id=workspace_id
)
field_name = f'{attribute_type}_count'
setattr(sage_intacct_count, field_name, count)
sage_intacct_count.save(update_fields=[field_name, 'updated_at'])
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

Fix critical bug: .get() doesn't return a tuple.

Line 2045 attempts to unpack a tuple from .get(), which returns a single object, not a tuple. This will raise ValueError: not enough values to unpack (expected 2, got 1) at runtime.

Apply this diff to fix:

-        sage_intacct_count, _ = SageIntacctAttributesCount.objects.get(
+        sage_intacct_count = SageIntacctAttributesCount.objects.get(
             workspace_id=workspace_id
         )

Alternatively, if the record might not exist and should be created automatically:

-        sage_intacct_count, _ = SageIntacctAttributesCount.objects.get(
+        sage_intacct_count, _ = SageIntacctAttributesCount.objects.get_or_create(
             workspace_id=workspace_id
         )

Additional consideration: The method doesn't validate that attribute_type corresponds to a valid field. Consider adding validation to prevent silent failures if an invalid attribute type is passed.

📝 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
@staticmethod
def update_attribute_count(workspace_id: int, attribute_type: str, count: int) -> None:
"""
Update attribute count for a workspace
:param workspace_id: Workspace ID
:param attribute_type: Type of attribute (e.g., 'accounts', 'vendors')
:param count: Count value from Sage Intacct
"""
sage_intacct_count, _ = SageIntacctAttributesCount.objects.get(
workspace_id=workspace_id
)
field_name = f'{attribute_type}_count'
setattr(sage_intacct_count, field_name, count)
sage_intacct_count.save(update_fields=[field_name, 'updated_at'])
@staticmethod
def update_attribute_count(workspace_id: int, attribute_type: str, count: int) -> None:
"""
Update attribute count for a workspace
:param workspace_id: Workspace ID
:param attribute_type: Type of attribute (e.g., 'accounts', 'vendors')
:param count: Count value from Sage Intacct
"""
sage_intacct_count = SageIntacctAttributesCount.objects.get(
workspace_id=workspace_id
)
field_name = f'{attribute_type}_count'
setattr(sage_intacct_count, field_name, count)
sage_intacct_count.save(update_fields=[field_name, 'updated_at'])
Suggested change
@staticmethod
def update_attribute_count(workspace_id: int, attribute_type: str, count: int) -> None:
"""
Update attribute count for a workspace
:param workspace_id: Workspace ID
:param attribute_type: Type of attribute (e.g., 'accounts', 'vendors')
:param count: Count value from Sage Intacct
"""
sage_intacct_count, _ = SageIntacctAttributesCount.objects.get(
workspace_id=workspace_id
)
field_name = f'{attribute_type}_count'
setattr(sage_intacct_count, field_name, count)
sage_intacct_count.save(update_fields=[field_name, 'updated_at'])
@staticmethod
def update_attribute_count(workspace_id: int, attribute_type: str, count: int) -> None:
"""
Update attribute count for a workspace
:param workspace_id: Workspace ID
:param attribute_type: Type of attribute (e.g., 'accounts', 'vendors')
:param count: Count value from Sage Intacct
"""
sage_intacct_count, _ = SageIntacctAttributesCount.objects.get_or_create(
workspace_id=workspace_id
)
field_name = f'{attribute_type}_count'
setattr(sage_intacct_count, field_name, count)
sage_intacct_count.save(update_fields=[field_name, 'updated_at'])
🤖 Prompt for AI Agents
In apps/sage_intacct/models.py around lines 2037 to 2050, the code incorrectly
unpacks the result of SageIntacctAttributesCount.objects.get(...) (get returns a
single object) which will raise a ValueError; change the retrieval to assign the
single object (e.g., sage_intacct_count =
SageIntacctAttributesCount.objects.get(...)) or use get_or_create(...) if the
record should be created when missing, then build the field name
f'{attribute_type}_count', validate that this attribute exists on the model
(e.g., hasattr(SageIntacctAttributesCount, field_name) or check _meta.get_field)
and raise a clear error if invalid, set the attribute with
setattr(sage_intacct_count, field_name, count) and save with
update_fields=[field_name, 'updated_at'] (or include creation logic when using
get_or_create).

@github-actions
Copy link

Coverage

Coverage Report
FileStmtsMissCoverMissing
apps
   exceptions.py471177%27, 33, 39–44, 50, 56–61, 67, 73, 79–80
apps/fyle
   actions.py124794%214, 263, 319–323
   constants.py10100% 
   helpers.py2352291%50–57, 66, 146–162, 376, 470, 477–480, 483–484
   models.py3362194%313–315, 319–321, 325–327, 331–333, 337–339, 343–345, 406, 516–537, 568, 585
   queue.py470100% 
   serializers.py410100% 
   signals.py35683%22, 55–56, 69–71
   tasks.py4487384%154–155, 158–159, 162–163, 185–186, 220–230, 242–247, 290–293, 314–318, 328–336, 371–372, 402–449, 733–737
   views.py1721591%84–91, 145, 245–246, 290–291, 316–320, 413–417
apps/internal
   actions.py39295%26–27
   helpers.py361364%18–35
   serializers.py300100% 
   views.py97199%39
apps/internal/services
   e2e_setup.py590100% 
   fixture_factory.py59395%218–220
apps/mappings
   constants.py20100% 
   exceptions.py66395%69–71
   helpers.py300100% 
   models.py500100% 
   schedules.py150100% 
   serializers.py100100% 
   signals.py1071289%84–86, 121, 174–179, 185–189, 195–198
   tasks.py1682685%105–106, 190–207, 237, 240, 243, 249, 252, 258, 297–299, 310, 382
   utils.py340100% 
   views.py46491%34–35, 114–115
apps/sage_intacct
   actions.py29486%21–22, 49–50
   dependent_fields.py2704085%315, 351–354, 383–392, 394–395, 397–399, 401–402, 404–405, 409–411, 455, 475, 515–517, 563–565, 613–619, 623–624
   helpers.py40295%51–52
   models.py9008491%58–59, 209, 316, 323, 367, 388–405, 464, 476, 526, 532, 567, 617–619, 633–646, 657, 693, 812, 851, 1047, 1138–1141, 1211, 1312–1332, 1410, 1426, 1834–1888, 1919, 1963–1999
   queue.py1631690%45, 53, 58, 105, 127–130, 170, 184, 199–202, 237, 258–261, 296, 318–321
   serializers.py350100% 
   tasks.py103913687%77–80, 114, 150–151, 155–158, 160, 163, 213, 247–248, 394, 400, 408, 572–573, 593–594, 640–641, 645, 664, 679, 684, 689, 696, 702, 716, 753–754, 821–822, 826, 847, 862, 868, 871–874, 881, 895–899, 936–937, 993–994, 996, 1017, 1032, 1038, 1041–1044, 1051, 1065–1069, 1105–1106, 1162–1163, 1166, 1187, 1202, 1208, 1214, 1221, 1227, 1241, 1293–1294, 1360–1369, 1411–1435, 1554–1558, 1614–1620, 1706–1716, 1719, 1728–1729, 1763–1765, 1799–1802, 1870–1874, 1883
   utils.py8764895%96, 108, 111, 536–537, 714–718, 859, 864, 875, 1165, 1181–1187, 1196–1197, 1672–1682, 1910–1914, 1942–1946, 1974–1978, 2020–2024, 2077–2081, 2101, 2110, 2119, 2128–2129, 2140, 2163–2165
   views.py1433377%126–128, 133–134, 140–141, 184–187, 256–286, 297–312, 325–329
apps/sage_intacct/errors
   errors.py20100% 
   helpers.py26196%73
apps/tasks
   models.py680100% 
   serializers.py60100% 
   views.py370100% 
apps/users
   helpers.py120100% 
   models.py530100% 
   views.py170100% 
apps/workspaces
   actions.py46980%65, 84–85, 103, 122–133, 147
   enums.py50100% 
   helpers.py80100% 
   models.py175498%338–340, 350–352
   permissions.py34974%33, 59–67
   serializers.py39197%53
   signals.py310100% 
   tasks.py154895%52–58, 186–193, 234, 266–267
   utils.py90100% 
   views.py262698%115–118, 484–485
apps/workspaces/apis/advanced_settings
   serializers.py80396%260, 263, 266
   triggers.py100100% 
   views.py110100% 
apps/workspaces/apis/errors
   serializers.py200100% 
   views.py150100% 
apps/workspaces/apis/export_settings
   helpers.py730100% 
   serializers.py103397%267, 270, 273
   triggers.py39295%32–33
   views.py110100% 
apps/workspaces/apis/import_settings
   serializers.py1411887%232–238, 243–249, 257–265, 283, 286, 314, 317–318, 326
   triggers.py47198%31
   views.py330100% 
fyle_integrations_imports
   dataclasses.py310100% 
   models.py230100% 
   queues.py34585%24, 99, 136, 148–149
   signals.py100100% 
   tasks.py926628%66–103, 112–118, 131–194, 198–213
fyle_integrations_imports/modules
   base.py182796%75, 91–92, 95, 224–225, 343
   categories.py1712983%79, 95, 98–99, 102–103, 270–271, 281, 294, 296, 340, 357–403
   cost_centers.py90891%131–132, 150–151, 160, 176, 178, 224
   expense_custom_fields.py121794%83–88, 247, 263, 265, 294
   merchants.py1002179%79–82, 110–128, 154–161, 177–178, 188, 201, 203, 233
   projects.py82594%107–108, 118, 142, 189
   tax_groups.py180100% 
   webhook_attributes.py127695%95, 97–99, 208–209
workers
   actions.py210100% 
   helpers.py410100% 
   worker.py56591%44–45, 77–78, 125
TOTAL859580691% 

Tests Skipped Failures Errors Time
488 0 💤 0 ❌ 0 🔥 42.983s ⏱️

@github-actions
Copy link


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

apps/sage_intacct/models.py (100%)
apps/sage_intacct/serializers.py (100%)
apps/sage_intacct/utils.py (98.8%): Missing lines 859
apps/sage_intacct/views.py (100%)
apps/workspaces/views.py (100%)

Total: 143 lines
Missing: 1 line
Coverage: 99%

@github-actions
Copy link

Coverage

Coverage Report
FileStmtsMissCoverMissing
apps
   exceptions.py471177%27, 33, 39–44, 50, 56–61, 67, 73, 79–80
apps/fyle
   actions.py124794%214, 263, 319–323
   constants.py10100% 
   helpers.py2352291%50–57, 66, 146–162, 376, 470, 477–480, 483–484
   models.py3362194%313–315, 319–321, 325–327, 331–333, 337–339, 343–345, 406, 516–537, 568, 585
   queue.py470100% 
   serializers.py410100% 
   signals.py35683%22, 55–56, 69–71
   tasks.py4487384%154–155, 158–159, 162–163, 185–186, 220–230, 242–247, 290–293, 314–318, 328–336, 371–372, 402–449, 733–737
   views.py1721591%84–91, 145, 245–246, 290–291, 316–320, 413–417
apps/internal
   actions.py39295%26–27
   helpers.py361364%18–35
   serializers.py300100% 
   views.py97199%39
apps/internal/services
   e2e_setup.py590100% 
   fixture_factory.py59395%218–220
apps/mappings
   constants.py20100% 
   exceptions.py66395%69–71
   helpers.py300100% 
   models.py500100% 
   schedules.py150100% 
   serializers.py100100% 
   signals.py1071289%84–86, 121, 174–179, 185–189, 195–198
   tasks.py1682685%105–106, 190–207, 237, 240, 243, 249, 252, 258, 297–299, 310, 382
   utils.py340100% 
   views.py46491%34–35, 114–115
apps/sage_intacct
   actions.py29486%21–22, 49–50
   dependent_fields.py2704085%315, 351–354, 383–392, 394–395, 397–399, 401–402, 404–405, 409–411, 455, 475, 515–517, 563–565, 613–619, 623–624
   helpers.py40295%51–52
   models.py9008491%58–59, 209, 316, 323, 367, 388–405, 464, 476, 526, 532, 567, 617–619, 633–646, 657, 693, 812, 851, 1047, 1138–1141, 1211, 1312–1332, 1410, 1426, 1834–1888, 1919, 1963–1999
   queue.py1631690%45, 53, 58, 105, 127–130, 170, 184, 199–202, 237, 258–261, 296, 318–321
   serializers.py350100% 
   tasks.py103913687%77–80, 114, 150–151, 155–158, 160, 163, 213, 247–248, 394, 400, 408, 572–573, 593–594, 640–641, 645, 664, 679, 684, 689, 696, 702, 716, 753–754, 821–822, 826, 847, 862, 868, 871–874, 881, 895–899, 936–937, 993–994, 996, 1017, 1032, 1038, 1041–1044, 1051, 1065–1069, 1105–1106, 1162–1163, 1166, 1187, 1202, 1208, 1214, 1221, 1227, 1241, 1293–1294, 1360–1369, 1411–1435, 1554–1558, 1614–1620, 1706–1716, 1719, 1728–1729, 1763–1765, 1799–1802, 1870–1874, 1883
   utils.py8764895%96, 108, 111, 536–537, 714–718, 859, 864, 875, 1165, 1181–1187, 1196–1197, 1672–1682, 1910–1914, 1942–1946, 1974–1978, 2020–2024, 2077–2081, 2101, 2110, 2119, 2128–2129, 2140, 2163–2165
   views.py1433377%126–128, 133–134, 140–141, 184–187, 256–286, 297–312, 325–329
apps/sage_intacct/errors
   errors.py20100% 
   helpers.py26196%73
apps/tasks
   models.py680100% 
   serializers.py60100% 
   views.py370100% 
apps/users
   helpers.py120100% 
   models.py530100% 
   views.py170100% 
apps/workspaces
   actions.py46980%65, 84–85, 103, 122–133, 147
   enums.py50100% 
   helpers.py80100% 
   models.py175498%338–340, 350–352
   permissions.py34974%33, 59–67
   serializers.py39197%53
   signals.py310100% 
   tasks.py154895%52–58, 186–193, 234, 266–267
   utils.py90100% 
   views.py262698%115–118, 484–485
apps/workspaces/apis/advanced_settings
   serializers.py80396%260, 263, 266
   triggers.py100100% 
   views.py110100% 
apps/workspaces/apis/errors
   serializers.py200100% 
   views.py150100% 
apps/workspaces/apis/export_settings
   helpers.py730100% 
   serializers.py103397%267, 270, 273
   triggers.py39295%32–33
   views.py110100% 
apps/workspaces/apis/import_settings
   serializers.py1411887%232–238, 243–249, 257–265, 283, 286, 314, 317–318, 326
   triggers.py47198%31
   views.py330100% 
fyle_integrations_imports
   dataclasses.py310100% 
   models.py230100% 
   queues.py34585%24, 99, 136, 148–149
   signals.py100100% 
   tasks.py926628%66–103, 112–118, 131–194, 198–213
fyle_integrations_imports/modules
   base.py182796%75, 91–92, 95, 224–225, 343
   categories.py1712983%79, 95, 98–99, 102–103, 270–271, 281, 294, 296, 340, 357–403
   cost_centers.py90891%131–132, 150–151, 160, 176, 178, 224
   expense_custom_fields.py121794%83–88, 247, 263, 265, 294
   merchants.py1002179%79–82, 110–128, 154–161, 177–178, 188, 201, 203, 233
   projects.py82594%107–108, 118, 142, 189
   tax_groups.py180100% 
   webhook_attributes.py127695%95, 97–99, 208–209
workers
   actions.py210100% 
   helpers.py410100% 
   worker.py56591%44–45, 77–78, 125
TOTAL859580691% 

Tests Skipped Failures Errors Time
488 0 💤 0 ❌ 0 🔥 40.889s ⏱️

@github-actions
Copy link


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

apps/sage_intacct/models.py (100%)
apps/sage_intacct/serializers.py (100%)
apps/sage_intacct/utils.py (98.8%): Missing lines 859
apps/sage_intacct/views.py (100%)
apps/workspaces/views.py (100%)

Total: 143 lines
Missing: 1 line
Coverage: 99%

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)
tests/sql_fixtures/reset_db_fixtures/reset_db.sql (1)

3806-3829: Add DEFAULT 0 to all 18 count columns to match ORM schema.

This issue was previously flagged and remains unresolved. All 18 integer count columns (accounts_count, items_count, vendors_count, employees_count, departments_count, classes_count, customers_count, projects_count, locations_count, expense_types_count, tax_details_count, cost_codes_count, cost_types_count, charge_card_accounts_count, payment_accounts_count, expense_payment_types_count, allocations_count) are marked NOT NULL without defaults. The Django ORM model defines all these with default=0, creating a mismatch. The fixture's initial COPY statement supplies zeros, but any direct SQL INSERT operations that omit these columns will fail.

Apply this diff to add DEFAULT 0 to all count columns:

 CREATE TABLE public.sage_intacct_attributes_count (
     id integer NOT NULL,
-    accounts_count integer NOT NULL,
-    items_count integer NOT NULL,
-    vendors_count integer NOT NULL,
-    employees_count integer NOT NULL,
-    departments_count integer NOT NULL,
-    classes_count integer NOT NULL,
-    customers_count integer NOT NULL,
-    projects_count integer NOT NULL,
-    locations_count integer NOT NULL,
-    expense_types_count integer NOT NULL,
-    tax_details_count integer NOT NULL,
-    cost_codes_count integer NOT NULL,
-    cost_types_count integer NOT NULL,
+    accounts_count integer NOT NULL DEFAULT 0,
+    items_count integer NOT NULL DEFAULT 0,
+    vendors_count integer NOT NULL DEFAULT 0,
+    employees_count integer NOT NULL DEFAULT 0,
+    departments_count integer NOT NULL DEFAULT 0,
+    classes_count integer NOT NULL DEFAULT 0,
+    customers_count integer NOT NULL DEFAULT 0,
+    projects_count integer NOT NULL DEFAULT 0,
+    locations_count integer NOT NULL DEFAULT 0,
+    expense_types_count integer NOT NULL DEFAULT 0,
+    tax_details_count integer NOT NULL DEFAULT 0,
+    cost_codes_count integer NOT NULL DEFAULT 0,
+    cost_types_count integer NOT NULL DEFAULT 0,
     user_defined_dimensions_details jsonb,
-    charge_card_accounts_count integer NOT NULL,
-    payment_accounts_count integer NOT NULL,
-    expense_payment_types_count integer NOT NULL,
-    allocations_count integer NOT NULL,
+    charge_card_accounts_count integer NOT NULL DEFAULT 0,
+    payment_accounts_count integer NOT NULL DEFAULT 0,
+    expense_payment_types_count integer NOT NULL DEFAULT 0,
+    allocations_count integer NOT NULL DEFAULT 0,
     created_at timestamp with time zone NOT NULL,
     updated_at timestamp with time zone NOT NULL,
     workspace_id integer NOT NULL
 );
🧹 Nitpick comments (1)
tests/test_sageintacct/test_utils.py (1)

2085-2085: Rename unused loop variable.

The name variable in the loop is never used. Rename it to _ to indicate it's intentionally unused.

Apply this diff:

-    for name, mock_path, sync_method, attr_type, model in sync_tests:
+    for _, mock_path, sync_method, attr_type, model in sync_tests:
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 12b68f6 and 1ee18e9.

📒 Files selected for processing (4)
  • apps/internal/migrations/0026_auto_generated_sql.py (1 hunks)
  • apps/sage_intacct/models.py (2 hunks)
  • tests/sql_fixtures/reset_db_fixtures/reset_db.sql (19 hunks)
  • tests/test_sageintacct/test_utils.py (10 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/sage_intacct/models.py
🧰 Additional context used
🧬 Code graph analysis (1)
tests/test_sageintacct/test_utils.py (2)
apps/sage_intacct/models.py (1)
  • SageIntacctAttributesCount (2002-2050)
apps/sage_intacct/utils.py (1)
  • is_sync_allowed (179-195)
🪛 Ruff (0.14.5)
tests/test_sageintacct/test_utils.py

2022-2022: Unused function argument: db

(ARG001)


2043-2043: Unused function argument: db

(ARG001)


2064-2064: Unused function argument: db

(ARG001)


2085-2085: Loop control variable name not used within loop body

Rename unused name to _name

(B007)


2105-2105: Unused function argument: db

(ARG001)


2118-2118: Unused function argument: field

(ARG001)


2118-2118: Unused function argument: value

(ARG001)


2118-2118: Unused function argument: updated_at

(ARG001)


2121-2121: Unused function argument: field

(ARG001)


2132-2132: Unused function argument: db

(ARG001)


2132-2132: Unused function argument: create_dependent_field_setting

(ARG001)

apps/internal/migrations/0026_auto_generated_sql.py

6-9: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


11-53: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: pytest
🔇 Additional comments (6)
tests/test_sageintacct/test_utils.py (4)

27-30: LGTM! Good addition of count mocking.

The added mocking for .count() API calls correctly supports the new attribute count tracking functionality. The return values align with the test assertions.

Also applies to: 78-81, 161-164, 187-190, 319-322, 1490-1493


2022-2061: LGTM! Comprehensive test coverage for sync limits.

The tests correctly verify the is_sync_allowed logic for both standard attributes (30K limit) and cost types (500K limit), including the workspace creation date threshold behavior.


2105-2130: LGTM! Proper test of allocation sync with timestamp.

The test correctly verifies that allocation sync respects the latest_updated_at parameter and persists the count to SageIntacctAttributesCount.


2132-2224: LGTM! Excellent comprehensive test of count persistence.

This test thoroughly verifies that all sync methods correctly persist their attribute counts to SageIntacctAttributesCount. The sequential structure with refresh_from_db() ensures each count update is properly saved.

apps/internal/migrations/0026_auto_generated_sql.py (1)

13-50: LGTM! Idempotent backfill logic.

The forward SQL correctly uses NOT EXISTS to ensure idempotent backfilling of attribute counts for existing workspaces. All fields are properly initialized.

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

3428-3468: intacct_sync_timestamps schema looks good.

The table definition for intacct_sync_timestamps is well-structured: all optional synced_at columns are nullable (allowing partial sync states), required audit fields (created_at, updated_at) are NOT NULL, workspace_id is properly constrained, and the GENERATED BY DEFAULT AS IDENTITY sequence is correctly configured.

WHERE sic.workspace_id = w.id
);
""",
reverse_sql=""" DELETE FROM sage_intacct_attributes_count WHERE EXISTS ( SELECT 1 FROM sage_intacct_attributes_count WHERE workspace_id = sage_intacct_attributes_count.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.

⚠️ Potential issue | 🟠 Major

Fix reverse SQL - will delete all rows.

The WHERE EXISTS clause will always evaluate to true for every row in the table, causing the reverse migration to delete all attribute count records, not just the backfilled ones.

Since distinguishing backfilled rows from organically created rows is not feasible, make this migration non-reversible.

Apply this diff:

-            reverse_sql=""" DELETE FROM sage_intacct_attributes_count WHERE EXISTS ( SELECT 1 FROM sage_intacct_attributes_count WHERE workspace_id = sage_intacct_attributes_count.workspace_id ); """
+            reverse_sql=migrations.RunSQL.noop
🤖 Prompt for AI Agents
In apps/internal/migrations/0026_auto_generated_sql.py around line 51, the
provided reverse SQL uses a WHERE EXISTS that will match every row and thus
delete all records; make this migration non-reversible instead of providing a
broken reverse SQL. Replace the current reverse_sql value with a non-reversible
marker (use Django's no-op reverse for RunSQL or set reverse_sql to None) and
add a short comment explaining that the reverse is intentionally omitted because
backfilled rows cannot be distinguished from real rows.

Comment on lines +10265 to +10267
COPY public.sage_intacct_attributes_count (id, accounts_count, items_count, vendors_count, employees_count, departments_count, classes_count, customers_count, projects_count, locations_count, expense_types_count, tax_details_count, cost_codes_count, cost_types_count, user_defined_dimensions_details, charge_card_accounts_count, payment_accounts_count, expense_payment_types_count, allocations_count, created_at, updated_at, workspace_id) FROM stdin;
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2022-09-20 08:38:48.66191+00 2022-09-20 08:38:48.661952+00 1
\.
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix invalid JSON value in jsonb column.

The COPY statement for sage_intacct_attributes_count contains invalid JSON in the user_defined_dimensions_details column. Line 10266 shows value 0, but this column is jsonb type (line 3821 of hunk 4). Valid JSON requires either \N (NULL), '{}' (empty object), or '[]' (empty array). The value 0 will fail JSON parsing.

Apply this diff to use NULL instead of 0:

-COPY public.sage_intacct_attributes_count (id, accounts_count, items_count, vendors_count, employees_count, departments_count, classes_count, customers_count, projects_count, locations_count, expense_types_count, tax_details_count, cost_codes_count, cost_types_count, user_defined_dimensions_details, charge_card_accounts_count, payment_accounts_count, expense_payment_types_count, allocations_count, created_at, updated_at, workspace_id) FROM stdin;
-1	0	0	0	0	0	0	0	0	0	0	0	0	0	0	0	0	0	0	2022-09-20 08:38:48.66191+00	2022-09-20 08:38:48.661952+00	1
+COPY public.sage_intacct_attributes_count (id, accounts_count, items_count, vendors_count, employees_count, departments_count, classes_count, customers_count, projects_count, locations_count, expense_types_count, tax_details_count, cost_codes_count, cost_types_count, user_defined_dimensions_details, charge_card_accounts_count, payment_accounts_count, expense_payment_types_count, allocations_count, created_at, updated_at, workspace_id) FROM stdin;
-1	0	0	0	0	0	0	0	0	0	0	0	0	0	\N	0	0	0	0	2022-09-20 08:38:48.66191+00	2022-09-20 08:38:48.661952+00	1
📝 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
COPY public.sage_intacct_attributes_count (id, accounts_count, items_count, vendors_count, employees_count, departments_count, classes_count, customers_count, projects_count, locations_count, expense_types_count, tax_details_count, cost_codes_count, cost_types_count, user_defined_dimensions_details, charge_card_accounts_count, payment_accounts_count, expense_payment_types_count, allocations_count, created_at, updated_at, workspace_id) FROM stdin;
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2022-09-20 08:38:48.66191+00 2022-09-20 08:38:48.661952+00 1
\.
COPY public.sage_intacct_attributes_count (id, accounts_count, items_count, vendors_count, employees_count, departments_count, classes_count, customers_count, projects_count, locations_count, expense_types_count, tax_details_count, cost_codes_count, cost_types_count, user_defined_dimensions_details, charge_card_accounts_count, payment_accounts_count, expense_payment_types_count, allocations_count, created_at, updated_at, workspace_id) FROM stdin;
1 0 0 0 0 0 0 0 0 0 0 0 0 0 \N 0 0 0 0 2022-09-20 08:38:48.66191+00 2022-09-20 08:38:48.661952+00 1
\.
🤖 Prompt for AI Agents
In tests/sql_fixtures/reset_db_fixtures/reset_db.sql around lines 10265-10267,
the COPY data row uses plain 0 for the jsonb column
user_defined_dimensions_details (column position matching hunk 4 line 3821),
which is invalid JSON; replace that 0 with the COPY NULL marker \N (or a valid
JSON literal like '{}' or '[]') so the row parses correctly and the column count
remains unchanged.

@ruuushhh ruuushhh merged commit 55e7769 into master Nov 24, 2025
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/XL Extra Large PR

Development

Successfully merging this pull request may close these issues.

3 participants