Skip to content

Comments

added a separately_billable to charge item def#3476

Open
praffq wants to merge 8 commits intodevelopfrom
prafful/feat/added-separately_billable-flag-in-cif
Open

added a separately_billable to charge item def#3476
praffq wants to merge 8 commits intodevelopfrom
prafful/feat/added-separately_billable-flag-in-cif

Conversation

@praffq
Copy link
Contributor

@praffq praffq commented Jan 19, 2026

Merge Checklist

  • Tests added/fixed
  • Update docs in /docs
  • Linting Complete
  • Any other necessary step

Only PR's with test cases included and passing lint and test pipelines will be reviewed

@ohcnetwork/care-backend-maintainers @ohcnetwork/care-backend-admins

Summary by CodeRabbit

  • New Features

    • Added separately billable status tracking for charge item definitions.
    • Introduced validation requiring service resource specification when manually applying non-separately-billable items.
    • Automatic management of separately billable status based on item associations.
  • Tests

    • Added extensive test coverage for separately billable charge items and automatic status management.

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

@praffq praffq requested a review from a team as a code owner January 19, 2026 11:32
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 19, 2026

📝 Walkthrough

Walkthrough

This PR introduces a separately_billable flag to ChargeItemDefinitions with associated validation, signal handlers for linked resources, utility functions for state management, and comprehensive test coverage to enforce billing behavior requirements.

Changes

Cohort / File(s) Summary
Model & Schema
care/emr/models/charge_item_definition.py, care/emr/resources/charge_item_definition/spec.py
Added separately_billable BooleanField (default: True) to model and spec; new attribute tracks whether charge items are billed independently.
API Validation
care/emr/api/viewsets/charge_item.py
Added validation in apply_charge_item_defs to enforce that non-separately-billable definitions require a linked service resource; raises ValidationError if constraint violated.
Signal Infrastructure
care/emr/signals/__init__.py, care/emr/signals/charge_item_def/__init__.py
Created signal package with new re-exports; package initializer aggregates activity_definition, product, and schedule submodules.
Signal Handlers
care/emr/signals/charge_item_def/activity_definition.py, care/emr/signals/charge_item_def/product.py, care/emr/signals/charge_item_def/schedule.py
Implemented pre\_save/post\_save signal handlers to track definition linkage changes across Product, Schedule, and ActivityDefinition models; automatically updates separately_billable state based on resource associations.
Utility Functions
care/emr/utils/charge_item_definition.py
Added three helpers: is_linked_to_any_resource() checks multi-model associations, recalculate_separately_billable() updates flag based on linkage, set_separately_billable_false() forces flag to False.
Database Migrations
care/emr/migrations/0066_chargeitemdefinition_separately_billable.py, care/emr/migrations/0067_sync_separately_billable_values.py
Schema migration adds field with default True; data migration syncs existing records, setting flag to False for already-linked definitions.
Test Coverage
care/emr/tests/test_charge_item_api.py, care/emr/tests/test_charge_item_def_signals.py
Added API validation tests (6 scenarios) and comprehensive signal handler tests (17 test methods covering Product, Schedule, ActivityDefinition linkage and deletion scenarios).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The description is missing the 'Proposed Changes' section which should explain what was changed; it only references an issue and includes an unchecked merge checklist. Add a 'Proposed Changes' section describing the changes made (field addition, validation logic, signal handlers, utilities, tests, and migrations).
Docstring Coverage ⚠️ Warning Docstring coverage is 12.20% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding a separately_billable field to the charge item definition model.

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

✨ 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 prafful/feat/added-separately_billable-flag-in-cif

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

@codecov
Copy link

codecov bot commented Jan 19, 2026

Codecov Report

❌ Patch coverage is 91.47287% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.59%. Comparing base (2eddc75) to head (8f4d27c).

Files with missing lines Patch % Lines
care/emr/utils/charge_item_definition.py 75.00% 4 Missing and 2 partials ⚠️
care/emr/signals/charge_item_def/schedule.py 89.74% 2 Missing and 2 partials ⚠️
...emr/signals/charge_item_def/activity_definition.py 96.77% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #3476      +/-   ##
===========================================
+ Coverage    74.48%   74.59%   +0.10%     
===========================================
  Files          471      476       +5     
  Lines        21667    21796     +129     
  Branches      2257     2280      +23     
===========================================
+ Hits         16139    16259     +120     
- Misses        5027     5031       +4     
- Partials       501      506       +5     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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

🤖 Fix all issues with AI agents
In `@care/emr/signals/charge_item_def/activity_definition.py`:
- Around line 29-49: The soft-delete branch in
update_separately_billable_activity currently returns early and skips
recalculating removed IDs; compute removed_ids = old_ids - new_ids (using
_OLD_CHARGE_ITEM_DEFS_IDS and instance.charge_item_definitions) before the
is_soft_deleted check and, when is_soft_deleted is true, call
recalculate_separately_billable for both each id in new_ids and each id in
removed_ids so removed charge_item_definitions are handled even on soft-delete;
keep existing calls to set_separately_billable_false for added_ids when not
deleted and retain original logic otherwise.

In `@care/emr/signals/charge_item_def/product.py`:
- Around line 27-43: The soft-delete branch in
update_separately_billable_product currently returns after only recalculating
the new_charge_item_def_id, skipping any recalculation for an
old_charge_item_def_id changed in the same save; update the handler so that when
is_soft_deleted is True and new_charge_item_def_id exists you also recalculates
the old_charge_item_def_id (if old_charge_item_def_id and old_charge_item_def_id
!= new_charge_item_def_id) before returning. Locate the receiver function
update_separately_billable_product and adjust the soft-delete branch to call
recalculate_separately_billable(old_charge_item_def_id) as appropriate (using
the existing _OLD_CHARGE_ITEM_DEF_ID, new_charge_item_def_id and old_deleted
checks) so both old and new IDs are handled.

In `@care/emr/signals/charge_item_def/schedule.py`:
- Around line 34-61: The soft-delete branch in
update_separately_billable_schedule currently only triggers recalculation for
new_charge_item_def_id/new_revisit_id and thus misses recalculating old IDs when
IDs changed during deletion; modify that branch so that before returning it also
calls recalculate_separately_billable for old_charge_item_def_id and
old_revisit_id when they exist and differ from the new IDs (i.e. mirror the
non-deleted change logic: if old_charge_item_def_id and old_charge_item_def_id
!= instance.charge_item_definition_id then
recalculate_separately_billable(old_charge_item_def_id), and similarly for
old_revisit_id), then keep the existing calls for new IDs and return.

In `@care/emr/utils/charge_item_definition.py`:
- Around line 7-21: The linkage checks in is_linked_to_any_resource currently
count soft-deleted rows; update each queryset (Product.objects.filter(...),
Schedule.objects.filter(...), the revisit_charge_item_definition_id query, and
ActivityDefinition.objects.filter(...charge_item_definitions__contains=...)) to
only consider non-deleted records by adding the appropriate soft-delete filter
for our models (e.g. add is_deleted=False or deleted_at__isnull=True depending
on our soft-delete field) so soft-deleted Product, Schedule, and
ActivityDefinition rows are excluded from the existence checks.
🧹 Nitpick comments (3)
care/emr/migrations/0058_sync_separately_billable_values.py (2)

10-20: Consider performance optimization for large datasets.

The current implementation has an N+1 query pattern – for each ChargeItemDefinition, four separate database queries are executed to check links. This could be... let's say, suboptimal on larger datasets.

Consider using prefetch/annotation or collecting linked IDs upfront:

♻️ Proposed optimization
 def sync_separately_billable(apps, schema_editor):
     ChargeItemDefinition = apps.get_model("emr", "ChargeItemDefinition")
     Product = apps.get_model("emr", "Product")
     Schedule = apps.get_model("emr", "Schedule")
     ActivityDefinition = apps.get_model("emr", "ActivityDefinition")

-    for charge_item_def in ChargeItemDefinition.objects.all():
-        is_linked = (
-            Product.objects.filter(charge_item_definition_id=charge_item_def.id).exists()
-            or Schedule.objects.filter(charge_item_definition_id=charge_item_def.id).exists()
-            or Schedule.objects.filter(revisit_charge_item_definition_id=charge_item_def.id).exists()
-            or ActivityDefinition.objects.filter(charge_item_definitions__contains=[charge_item_def.id]).exists()
-        )
-
-        if is_linked and charge_item_def.separately_billable:
-            charge_item_def.separately_billable = False
-            charge_item_def.save(update_fields=["separately_billable"])
+    # Collect all linked ChargeItemDefinition IDs upfront
+    linked_ids = set()
+    linked_ids.update(
+        Product.objects.exclude(charge_item_definition__isnull=True)
+        .values_list("charge_item_definition_id", flat=True)
+    )
+    linked_ids.update(
+        Schedule.objects.exclude(charge_item_definition__isnull=True)
+        .values_list("charge_item_definition_id", flat=True)
+    )
+    linked_ids.update(
+        Schedule.objects.exclude(revisit_charge_item_definition__isnull=True)
+        .values_list("revisit_charge_item_definition_id", flat=True)
+    )
+    for activity_def in ActivityDefinition.objects.exclude(charge_item_definitions=[]):
+        linked_ids.update(activity_def.charge_item_definitions or [])
+
+    # Bulk update
+    ChargeItemDefinition.objects.filter(
+        id__in=linked_ids, separately_billable=True
+    ).update(separately_billable=False)

29-31: Missing reverse migration function.

The RunPython operation lacks a reverse function, which means migrate --fake or rollbacks won't be fully reversible. It would be quite nice to have a migrations.RunPython.noop at minimum, or a proper reverse function if the original state needs restoration.

♻️ Proposed fix
     operations = [
-        migrations.RunPython(sync_separately_billable),
+        migrations.RunPython(sync_separately_billable, migrations.RunPython.noop),
     ]
care/emr/tests/test_charge_item_def_signals.py (1)

129-170: Consider adding test coverage for revisit_charge_item_definition unlink.

The Schedule tests cover the revisit_charge_item_definition link scenario but, interestingly, don't test unlinking or changing the revisit_charge_item_definition field. For consistency with the charge_item_definition tests, you might want to add those cases.

💡 Suggested additional tests
def test_schedule_revisit_unlink_recalculates_separately_billable(self):
    charge_item_def = self.create_charge_item_definition()
    schedule = self.create_schedule(revisit_charge_item_definition=charge_item_def)

    charge_item_def.refresh_from_db()
    self.assertFalse(charge_item_def.separately_billable)

    schedule.revisit_charge_item_definition = None
    schedule.save()

    charge_item_def.refresh_from_db()
    self.assertTrue(charge_item_def.separately_billable)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@care/emr/migrations/0062_sync_seprately_billable_values.py`:
- Line 1: The migration filename has a typo ("seprately"); rename the file to
0062_sync_separately_billable_values.py and update any references to that
migration if present (e.g., explicit imports or dependency entries in other
migration files) so Django continues to detect the migration; verify git history
move/rename is a proper rename (git mv) to preserve blame and that the migration
module still contains the original Migration class and operations.
🧹 Nitpick comments (2)
care/emr/migrations/0062_sync_seprately_billable_values.py (2)

10-20: Consider optimizing for larger datasets.

The current approach executes up to 4 queries per ChargeItemDefinition record (N+1 pattern). For a one-time migration on a small dataset, this is probably fine. For larger datasets, you might want to consider a more efficient approach—or not, if you enjoy watching progress bars.

♻️ Suggested optimization using bulk update
 def sync_separately_billable(apps, schema_editor):
     ChargeItemDefinition = apps.get_model("emr", "ChargeItemDefinition")
     Product = apps.get_model("emr", "Product")
     Schedule = apps.get_model("emr", "Schedule")
     ActivityDefinition = apps.get_model("emr", "ActivityDefinition")
 
-    for charge_item_def in ChargeItemDefinition.objects.all():
-        is_linked = (
-            Product.objects.filter(charge_item_definition_id=charge_item_def.id).exists()
-            or Schedule.objects.filter(charge_item_definition_id=charge_item_def.id).exists()
-            or Schedule.objects.filter(revisit_charge_item_definition_id=charge_item_def.id).exists()
-            or ActivityDefinition.objects.filter(charge_item_definitions__contains=[charge_item_def.id]).exists()
-        )
-
-        if is_linked and charge_item_def.separately_billable:
-            charge_item_def.separately_billable = False
-            charge_item_def.save(update_fields=["separately_billable"])
+    # Collect all linked ChargeItemDefinition IDs
+    linked_ids = set()
+
+    linked_ids.update(
+        Product.objects.exclude(charge_item_definition_id__isnull=True)
+        .values_list("charge_item_definition_id", flat=True)
+    )
+    linked_ids.update(
+        Schedule.objects.exclude(charge_item_definition_id__isnull=True)
+        .values_list("charge_item_definition_id", flat=True)
+    )
+    linked_ids.update(
+        Schedule.objects.exclude(revisit_charge_item_definition_id__isnull=True)
+        .values_list("revisit_charge_item_definition_id", flat=True)
+    )
+
+    # For ActivityDefinition with ArrayField, iterate but only once
+    for activity_def in ActivityDefinition.objects.exclude(charge_item_definitions=[]):
+        linked_ids.update(activity_def.charge_item_definitions)
+
+    # Bulk update all linked items
+    ChargeItemDefinition.objects.filter(
+        id__in=linked_ids, separately_billable=True
+    ).update(separately_billable=False)

29-31: Consider adding a reverse migration for rollback support.

The RunPython operation lacks a reverse_code function. If you ever need to roll back this migration, Django will refuse unless you provide one (or use migrations.RunPython.noop). I'm sure that won't be a problem though.

♻️ Add noop reverse if rollback isn't meaningful
     operations = [
-        migrations.RunPython(sync_separately_billable),
+        migrations.RunPython(sync_separately_billable, migrations.RunPython.noop),
     ]

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Separately Billable Charge Item Definitions

1 participant