Skip to content

Conversation

@sravfeyn
Copy link
Member

Product Description

https://dimagi.atlassian.net/browse/CCCT-1864

No product facing changes.

Technical Summary

  • Adds Currency, Country models and data from ISO data sources.
  • Sets currency_fk, country fields on opportunity and program based on existing currency field

I have run a script to analyze current prod opportunity/currency data to check invalid currencies etc. I have compiled the detailed data here in the doc. In summary

  • Total Opportunities: 822
  • Valid Currency code Opportunities: 808
    • Currency corresponds to exactly one country → country auto-set possible: 656
    • Currency corresponds to multiple countries → ambiguous: 148 (for e.g. USD, EUR etc) (Exact opportunity list in the doc). For these country won't be set
  • Invalid currency (constant mismatch): 14 (Exact opportunity list in the doc)
    • These currencies will get created with invalid=True

Safety Assurance

Safety story

Tested locally. Added relevant tests, and ran the analysis on prod

Automated test coverage

Added

QA Plan

Will be testing on staging to make sure migration goes out smoothly.

Labels & Review

  • The set of people pinged as reviewers is appropriate for the level of risk of the change

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 14, 2025

Walkthrough

Adds new models Currency and Country, and adds nullable ForeignKey fields currency_fk and country to Opportunity and Program while retaining the existing denormalized currency CharField on Opportunity. Introduces a shared data-migration utility populate_currency_and_country_fk_for_model, an opportunity migration that seeds Currency/Country and populates FKs, and a program migration that adds the program FKs and invokes the utility. Updates tests, fixtures, factories, forms, templates, views, tables, tasks, and utilities to use currency_fk and the currency_code property where appropriate.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • commcare_connect/program/utils.py — verify batching, currency normalization, mapping creation, and correctness of bulk_update usage.
  • commcare_connect/opportunity/migrations/0092_currency_country_opportunity_currency_fk.py — review seed lists, CreateModel definitions, RunPython functions, idempotency (ignore_conflicts), and on_delete semantics.
  • commcare_connect/program/migrations/0004_program_currency_fk.py — confirm migration dependencies and correct invocation of the shared population utility.
  • commcare_connect/opportunity/tests/test_models.py and commcare_connect/conftest.py — inspect dynamic import usage, autouse fixture behavior, and assertions around migration population.
  • commcare_connect/opportunity/models.py and commcare_connect/program/models.py — validate FK declarations, nullability, and preservation of Opportunity.currency field.
  • Forms, templates, views, tables, tasks, factories, reports, and utils — scan for consistent replacement of currency with currency_fk/currency_code, and ensure fallback behavior where currency FK is null.

Possibly related PRs

Suggested labels

QA Complete

Suggested reviewers

  • calellowitz
  • pxwxnvermx
  • hemant10yadav

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.27% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: adding Currency fields and setting currency_fk and country fields on the models.
Description check ✅ Passed The description provides relevant context including Jira reference, technical summary of the Currency/Country model additions, production data analysis, safety assurance, and testing plans.
✨ 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 sr/currency_fk

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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (5)
commcare_connect/program/utils.py (1)

47-47: Drop unused # noqa: E203 to satisfy Ruff

Ruff flagged this as an unused noqa (RUF100). Since qs[start : start + BATCH_SIZE] is fine as‑is, you can remove the # noqa: E203 comment.

commcare_connect/opportunity/migrations/0084_currency_country_opportunity_currency_fk.py (1)

411-411: Use a plain ASCII apostrophe in "Pa’anga" to avoid encoding/lint issues

Ruff flagged the RIGHT SINGLE QUOTATION MARK here (RUF001). You can use a normal ASCII apostrophe instead:

-    ("Pa’anga", "TOP"),
+    ("Pa'anga", "TOP"),
commcare_connect/program/models.py (1)

4-17: Program FKs align with Opportunity, but there are now two currency fields

The new currency_fk and country FKs are defined consistently with Opportunity and with the migrations, which is good. Note that Program now has both currency (CharField) and currency_fk (FK), so going forward it’d be good to treat one as the canonical source of truth (or add a sync path) to avoid the two drifting apart over time.

commcare_connect/opportunity/models.py (2)

62-66: Add __str__ method for better representation.

The Currency model would benefit from a __str__ method to improve its representation in the Django admin interface and debugging output.

Apply this diff to add a __str__ method:

 class Currency(models.Model):
     code = models.CharField(max_length=3, primary_key=True)  # ISO 4217
     name = models.CharField(max_length=64)
     is_valid = models.BooleanField(default=True)
+
+    def __str__(self):
+        return f"{self.code} - {self.name}"

68-72: Add __str__ method for better representation.

The Country model would benefit from a __str__ method to improve its representation in the Django admin interface and debugging output.

Apply this diff to add a __str__ method:

 class Country(models.Model):
     code = models.CharField(max_length=3, primary_key=True)  # ISO 3166-1 alpha-3
     name = models.CharField(max_length=128)
     currency = models.ForeignKey(Currency, on_delete=models.SET_NULL, null=True, blank=True)
+
+    def __str__(self):
+        return self.name
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 38a56c0 and b41db4b.

📒 Files selected for processing (6)
  • commcare_connect/opportunity/migrations/0084_currency_country_opportunity_currency_fk.py (1 hunks)
  • commcare_connect/opportunity/models.py (2 hunks)
  • commcare_connect/opportunity/tests/test_models.py (3 hunks)
  • commcare_connect/program/migrations/0003_program_currency_fk.py (1 hunks)
  • commcare_connect/program/models.py (2 hunks)
  • commcare_connect/program/utils.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
commcare_connect/program/models.py (1)
commcare_connect/opportunity/models.py (4)
  • Country (68-71)
  • Currency (62-65)
  • DeliveryType (53-59)
  • Opportunity (74-227)
commcare_connect/program/utils.py (1)
commcare_connect/opportunity/models.py (2)
  • Currency (62-65)
  • Country (68-71)
commcare_connect/program/migrations/0003_program_currency_fk.py (2)
commcare_connect/program/utils.py (1)
  • populate_currency_and_country_fk_for_model (26-70)
commcare_connect/opportunity/migrations/0084_currency_country_opportunity_currency_fk.py (1)
  • Migration (472-525)
commcare_connect/opportunity/migrations/0084_currency_country_opportunity_currency_fk.py (3)
commcare_connect/program/utils.py (1)
  • populate_currency_and_country_fk_for_model (26-70)
commcare_connect/opportunity/models.py (2)
  • Currency (62-65)
  • Country (68-71)
commcare_connect/program/migrations/0003_program_currency_fk.py (1)
  • Migration (13-39)
commcare_connect/opportunity/tests/test_models.py (2)
commcare_connect/opportunity/tests/factories.py (1)
  • OpportunityFactory (47-64)
commcare_connect/opportunity/migrations/0084_currency_country_opportunity_currency_fk.py (1)
  • populate_currency_and_country_fk (468-469)
🪛 Ruff (0.14.4)
commcare_connect/program/utils.py

47-47: Unused noqa directive (non-enabled: E203)

Remove unused noqa directive

(RUF100)

commcare_connect/program/migrations/0003_program_currency_fk.py

9-9: Unused function argument: schema_editor

(ARG001)


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

(RUF012)


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

(RUF012)

commcare_connect/opportunity/migrations/0084_currency_country_opportunity_currency_fk.py

411-411: String contains ambiguous (RIGHT SINGLE QUOTATION MARK). Did you mean ``` (GRAVE ACCENT)?

(RUF001)


433-433: Unused function argument: schema_editor

(ARG001)


468-468: Unused function argument: schema_editor

(ARG001)


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

(RUF012)


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

(RUF012)

🔇 Additional comments (4)
commcare_connect/program/utils.py (1)

26-68: Migration FK backfill utility looks solid and idempotent

The overall approach here (preloading Currency/Country, normalizing codes, creating invalid placeholders via get_or_create, and backfilling via bulk_update in batches) looks correct and should be safely idempotent, including for invalid/unknown codes and multi‑country currencies.

commcare_connect/opportunity/migrations/0084_currency_country_opportunity_currency_fk.py (1)

433-524: Currency/country seeding and FK wiring are consistent and idempotent

The migration correctly seeds Currency and Country via bulk_create, builds a code→Currency map, and uses the shared FK backfill utility for Opportunity. With ignore_conflicts=True on currencies and the delegated backfill, this should be safe to run once and re‑run (in tests) without duplicate rows or broken mappings.

commcare_connect/program/migrations/0003_program_currency_fk.py (1)

9-39: Program FK migration correctly delegates to shared backfill logic

Dependencies, FK definitions, and the RunPython step all look correct, and delegating to populate_currency_and_country_fk_for_model ensures Program gets the same currency/country population behavior as Opportunity without duplicating logic.

commcare_connect/opportunity/tests/test_models.py (1)

1-184: New migration test gives good end‑to‑end coverage of FK backfill behavior

The test_populate_currency_and_country_fk setup and assertions look solid: it covers valid, single‑country, multi‑country, and invalid currency cases, verifies is_valid=False for placeholders, and checks that only currencies (not countries) are created for invalid codes. Calling the migration function with django_apps is appropriate given how the shared util uses apps.get_model.

Copy link
Collaborator

@calellowitz calellowitz left a comment

Choose a reason for hiding this comment

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

One real suggestion, one nit


def populate_currency_and_country_fk_for_model(apps, model_name, app_label, total_label):
"""
Migration util to populate currency_fk and country fields for a opportunity/program
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: If this is just for use by the migration, I think it can go inline in the migration file

Copy link
Member Author

Choose a reason for hiding this comment

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

It's used in two migration files, so I had to pull this into a separate file.

Copy link
Collaborator

Choose a reason for hiding this comment

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

why does it need to be run twice?

Copy link
Member Author

Choose a reason for hiding this comment

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

There are two models Opportunity and Program that have currency field. So this is used in those two migration files as a common helper.

Country(
code=code,
name=name,
currency=code_to_currency.get(currency_code),
Copy link
Contributor

Choose a reason for hiding this comment

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

Are there missing currencies? Why are we expecting code_to_currency to not contain currency_code?

Copy link
Member Author

@sravfeyn sravfeyn Nov 18, 2025

Choose a reason for hiding this comment

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

Yes, there are invalid currencies. Please see the relevant note the description above

I have run a script to analyze current prod opportunity/currency data to check invalid currencies etc. I have compiled the detailed data here in the doc.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we should fix the ones with invalid currencies, since that breaks a bunch of other behavior, rather than leaving them in that state (and enforce it going forwards). Most look like test ones, or like the intended currency is clear, but if there are real ones with open questions we can reach out to the program teams, because it is unlikely those opportunities will be working correctly.

Copy link
Member Author

Choose a reason for hiding this comment

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

and enforce it going forwards

Yes, with this PR, currency validation is enforced by displaying only valid currencies to the user.

Regarding fixing the invalid currency opportunities, all the 14 invalid currency opportunities are test opportunities, only 3 of them are active (have end-date in future).

So, these can safely be left as they are, and this PR provides the option to update these currencies after the release as well if needed.

This design actually allows releasing this PR without the need for co-ordinating with the opportunity owners, as it changes the underlying fields without any new constraints. Otherwise, at the time of deploy, I will have to re-run the scripts, go through any new invalid currencies and co-ordinate with opportunity owners to fix them.

This point was indeed discussed and planned as such during the tech spec discussion as well. So, this is following the same plan.

class Country(models.Model):
code = models.CharField(max_length=3, primary_key=True) # ISO 3166-1 alpha-3
name = models.CharField(max_length=128)
currency = models.ForeignKey(Currency, on_delete=models.SET_NULL, null=True, blank=True)
Copy link
Contributor

Choose a reason for hiding this comment

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

Ties in with my question in the relating migration file - why is this a nullable field?

Copy link
Member Author

Choose a reason for hiding this comment

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

There are some countries with no official currency, please see the COUNTRIES data in the migration file.

@sravfeyn
Copy link
Member Author

@calellowitz Thanks for the review, PR is updated with your inputs.

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 (3)
commcare_connect/opportunity/migrations/0087_currency_country_opportunity_currency_fk.py (3)

266-430: Tidy up CURRENCIES data (duplicate code and ambiguous apostrophe).

Two minor data/maintainability nits in CURRENCIES:

  • ("Moroccan Dirham", "MAD") appears twice (Lines 372 and 426). It’s harmless because code is the PK and ignore_conflicts=True is used, but the duplicate entry is confusing and can be removed.
  • "Pa’anga" (Line 411) uses a RIGHT SINGLE QUOTATION MARK, which Ruff flags as ambiguous (RUF001) and can cause copy/paste surprises. Consider using a plain ASCII apostrophe instead.

Neither affects runtime behavior, but fixing them will keep the dataset and linters clean.


433-468: Rename schema_editor args to avoid Ruff ARG001 without changing behavior.

Both load_currency_and_country_data and populate_currency_and_country_fk accept schema_editor but don’t use it, which Ruff flags (ARG001). Django only cares about argument order, not names, so you can safely rename them to _schema_editor to satisfy the linter:

-def load_currency_and_country_data(apps, schema_editor):
+def load_currency_and_country_data(apps, _schema_editor):

- def populate_currency_and_country_fk(apps, schema_editor):
+def populate_currency_and_country_fk(apps, _schema_editor):

This keeps the migration API intact while quieting the warnings.


468-524: Consider freezing FK‑population logic inside the migration instead of importing from program.utils.

populate_currency_and_country_fk calls populate_currency_and_country_fk_for_model from commcare_connect.program.utils. That utility will evolve over time, but migrations are meant to be historical snapshots. On a fresh install in a year, this migration will run with whatever the current implementation of populate_currency_and_country_fk_for_model is, which can subtly change or break the behavior you intended when the migration was authored.

Consider copying the current implementation (or a slimmed‑down version of it) into this migration file so that the data migration logic is frozen in time and independent of future app‑level refactors.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b41db4b and 21bb702.

📒 Files selected for processing (3)
  • commcare_connect/opportunity/migrations/0087_currency_country_opportunity_currency_fk.py (1 hunks)
  • commcare_connect/opportunity/models.py (2 hunks)
  • commcare_connect/program/models.py (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • commcare_connect/opportunity/models.py
  • commcare_connect/program/models.py
🧰 Additional context used
🧬 Code graph analysis (1)
commcare_connect/opportunity/migrations/0087_currency_country_opportunity_currency_fk.py (2)
commcare_connect/program/utils.py (1)
  • populate_currency_and_country_fk_for_model (26-70)
commcare_connect/opportunity/models.py (2)
  • Currency (61-64)
  • Country (67-70)
🪛 Ruff (0.14.5)
commcare_connect/opportunity/migrations/0087_currency_country_opportunity_currency_fk.py

411-411: String contains ambiguous (RIGHT SINGLE QUOTATION MARK). Did you mean ``` (GRAVE ACCENT)?

(RUF001)


433-433: Unused function argument: schema_editor

(ARG001)


468-468: Unused function argument: schema_editor

(ARG001)


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

(RUF012)


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

(RUF012)

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

🧹 Nitpick comments (3)
commcare_connect/opportunity/tests/test_models.py (1)

165-184: Test assertions validate the core migration behavior correctly.

The test properly verifies:

  • Valid currencies populate currency_fk
  • Single-country currencies auto-set both currency_fk and country
  • Multi-country currencies leave country as NULL
  • Invalid currencies create placeholder Currency records with is_valid=False

Optional: Consider adding a test case for a valid currency that exists but isn't used by any country to ensure complete edge case coverage.

commcare_connect/opportunity/migrations/0087_currency_country_opportunity_currency_fk.py (2)

6-6: Note the cross-app dependency in migration.

Importing populate_currency_and_country_fk_for_model from program.utils creates a cross-app dependency. While this promotes code reuse, it means this migration will fail if the program app structure changes. Consider documenting this dependency or inlining the logic if isolation is preferred.

For better migration isolation, the utility could be defined inline within this migration file, though that would create duplication with the program migration.


478-498: Note the on_delete policy differences.

The Country model uses on_delete=SET_NULL for its currency FK (line 494), which allows graceful handling of currency deletion. However, the Opportunity FKs added later (lines 502-504, 509-511) use DO_NOTHING, creating an inconsistency. Consider whether Opportunity should use PROTECT to prevent accidental deletion of reference data.

Based on the models being reference data that shouldn't be deleted, PROTECT would be more explicit:

         migrations.AddField(
             model_name="opportunity",
             name="currency_fk",
             field=models.ForeignKey(
-                null=True, on_delete=django.db.models.deletion.DO_NOTHING, to="opportunity.currency"
+                null=True, on_delete=django.db.models.deletion.PROTECT, to="opportunity.currency"
             ),
         ),
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 21bb702 and bcc68ca.

📒 Files selected for processing (3)
  • commcare_connect/opportunity/migrations/0087_currency_country_opportunity_currency_fk.py (1 hunks)
  • commcare_connect/opportunity/tests/test_models.py (3 hunks)
  • commcare_connect/program/migrations/0003_program_currency_fk.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
commcare_connect/opportunity/tests/test_models.py (2)
commcare_connect/opportunity/tests/factories.py (1)
  • OpportunityFactory (47-64)
commcare_connect/opportunity/migrations/0087_currency_country_opportunity_currency_fk.py (1)
  • populate_currency_and_country_fk (468-469)
commcare_connect/opportunity/migrations/0087_currency_country_opportunity_currency_fk.py (3)
commcare_connect/program/utils.py (1)
  • populate_currency_and_country_fk_for_model (26-70)
commcare_connect/opportunity/models.py (2)
  • Currency (61-64)
  • Country (67-70)
commcare_connect/program/migrations/0003_program_currency_fk.py (1)
  • Migration (13-39)
commcare_connect/program/migrations/0003_program_currency_fk.py (2)
commcare_connect/program/utils.py (1)
  • populate_currency_and_country_fk_for_model (26-70)
commcare_connect/opportunity/migrations/0087_currency_country_opportunity_currency_fk.py (1)
  • Migration (472-525)
🪛 Ruff (0.14.5)
commcare_connect/opportunity/migrations/0087_currency_country_opportunity_currency_fk.py

411-411: String contains ambiguous (RIGHT SINGLE QUOTATION MARK). Did you mean ``` (GRAVE ACCENT)?

(RUF001)


433-433: Unused function argument: schema_editor

(ARG001)


468-468: Unused function argument: schema_editor

(ARG001)


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

(RUF012)


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

(RUF012)

commcare_connect/program/migrations/0003_program_currency_fk.py

9-9: Unused function argument: schema_editor

(ARG001)


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

(RUF012)


19-39: 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 (8)
commcare_connect/opportunity/tests/test_models.py (2)

1-4: LGTM - Imports support migration testing.

The importlib and django_apps imports are correctly used to dynamically load the migration module and retrieve model classes for testing the FK population logic.


15-15: LGTM - Factory import for test data creation.

The OpportunityFactory import is necessary for creating test opportunities with specific currency values.

commcare_connect/program/migrations/0003_program_currency_fk.py (3)

9-10: LGTM - Standard Django migration pattern.

The unused schema_editor parameter is required by Django's RunPython signature. The static analysis warning is a false positive.


20-33: Verify the on_delete=DO_NOTHING policy for reference data.

Both currency_fk and country use on_delete=DO_NOTHING, which means orphaned references could occur if Currency or Country records are deleted. While reference data typically shouldn't be deleted, using PROTECT would make this constraint explicit and prevent accidental deletions.

Consider whether PROTECT would be more appropriate:

field=models.ForeignKey(
    null=True, on_delete=django.db.models.deletion.PROTECT, to="opportunity.currency"
),

This matches the pattern used in the Opportunity model Currency/Country fields shown in the relevant code snippets. Based on learnings from the PR objectives, these are reference data models that should remain stable.


34-38: LGTM - RunPython configuration is appropriate.

The run_on_secondary: False hint prevents unnecessary execution on read replicas, and the noop reverse is acceptable since field removal will drop the data regardless.

commcare_connect/opportunity/migrations/0087_currency_country_opportunity_currency_fk.py (3)

9-263: LGTM - Comprehensive country data from ISO sources.

The COUNTRIES data is well-structured with ISO 3166-1 alpha-3 codes and appropriate handling of territories without official currencies (None values).


468-470: LGTM - Standard delegation to utility function.

The unused schema_editor parameter is required by Django's RunPython signature. The static analysis warning is a false positive.


513-524: LGTM - RunPython operations ordered correctly.

The migration properly sequences operations: first loading reference data, then populating FK relationships. The run_on_secondary: False hints are appropriate for data migrations.

Comment on lines +140 to +148
# Ensure currency / country rows exist (loaded via migration)
single_country_currency = "KES" # Kenya only
multi_country_currency = "USD" # Multiple ISO countries share USD
valid_currency_only = "INR"

assert CurrencyModel.objects.filter(code=valid_currency_only).exists()
assert CountryModel.objects.filter(code="KEN", currency_id=single_country_currency).exists()
assert CurrencyModel.objects.filter(code=multi_country_currency).exists()

Copy link
Contributor

@coderabbitai coderabbitai bot Nov 22, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Misleading variable name: INR is a single-country currency.

The variable valid_currency_only suggests INR only validates the currency FK without setting country, but based on the COUNTRIES data in the migration, INR maps to India ("IND", "India", "INR"), making it a single-country currency like KES. Consider renaming to avoid confusion.

Apply this diff:

-    valid_currency_only = "INR"
+    valid_single_country = "INR"  # India only
 
     assert CurrencyModel.objects.filter(code=valid_currency_only).exists()
     assert CountryModel.objects.filter(code="KEN", currency_id=single_country_currency).exists()

And update the assertion at line 169:

     # valid currency should map to fk
-    assert opp_valid.currency_fk_id == valid_currency_only
+    assert opp_valid.currency_fk_id == valid_single_country
+    # INR is single-country, should also set country
+    assert opp_valid.country_id == CountryModel.objects.get(code="IND").pk
📝 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
# Ensure currency / country rows exist (loaded via migration)
single_country_currency = "KES" # Kenya only
multi_country_currency = "USD" # Multiple ISO countries share USD
valid_currency_only = "INR"
assert CurrencyModel.objects.filter(code=valid_currency_only).exists()
assert CountryModel.objects.filter(code="KEN", currency_id=single_country_currency).exists()
assert CurrencyModel.objects.filter(code=multi_country_currency).exists()
# Ensure currency / country rows exist (loaded via migration)
single_country_currency = "KES" # Kenya only
multi_country_currency = "USD" # Multiple ISO countries share USD
valid_single_country = "INR" # India only
assert CurrencyModel.objects.filter(code=valid_single_country).exists()
assert CountryModel.objects.filter(code="KEN", currency_id=single_country_currency).exists()
assert CurrencyModel.objects.filter(code=multi_country_currency).exists()
🤖 Prompt for AI Agents
In commcare_connect/opportunity/tests/test_models.py around lines 140 to 148,
the variable valid_currency_only is misleading because INR is actually a
single-country currency per the migrations; rename it to something like
single_country_currency_ind (or valid_single_country_currency) and set it to
"INR", and then update the assertion at line 169 to assert that
CountryModel.objects.filter(code="IND", currency_id="INR").exists() so the test
checks India is associated with INR instead of implying INR is multi-country.

Copy link
Member Author

Choose a reason for hiding this comment

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

Minor, unnecessary, resolving it.

Copy link
Contributor

Choose a reason for hiding this comment

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

@sravfeyn, understood! I'll close this thread. Thanks for the feedback.

@sravfeyn
Copy link
Member Author

@calellowitz Bumping this for re-review/approval

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 (4)
commcare_connect/opportunity/migrations/0087_currency_country_opportunity_currency_fk.py (4)

266-430: Static currency dataset looks good; consider removing duplicated MAD entry.

CURRENCIES includes "Moroccan Dirham" / MAD twice (around Lines 372 and 426). This is harmless because of ignore_conflicts=True on bulk_create, but dropping the later duplicate would keep the dataset minimal and avoid a redundant insert attempt.

You can remove the extra entry like:

-    ("Moroccan Dirham", "MAD"),

433-470: RunPython helpers are correct; optionally silence Ruff ARG001 more cleanly.

Both migration functions correctly follow Django’s (apps, schema_editor) signature, and the data-loading / FK-population logic looks sound. Since schema_editor isn’t used, you can optionally silence Ruff’s ARG001 by renaming the parameter instead of adding ignores:

-def load_currency_and_country_data(apps, schema_editor):
+def load_currency_and_country_data(apps, _schema_editor):

-def populate_currency_and_country_fk(apps, schema_editor):
+def populate_currency_and_country_fk(apps, _schema_editor):

Django passes these arguments positionally, so the name change is safe.


6-7: Consider avoiding migration-time dependency on commcare_connect.program.utils.

Importing and calling populate_currency_and_country_fk_for_model from commcare_connect.program.utils is convenient but couples this migration to non-migration code that might change or be removed later, breaking new environments running migrations from scratch.

If you expect that helper to evolve, consider inlining a stable copy of the logic here (or in a dedicated migrations_utils module that is treated as versioned migration code) so this migration remains self-contained over time.

Also applies to: 468-470


472-525: Migration operations and model definitions align correctly; Ruff RUF012 is safe to ignore or filter.

The ordering—CreateModel for Currency and Country, then AddField on Opportunity, then the two RunPython steps—matches the models and ensures data exists before FK population, which looks correct.

Ruff’s RUF012 warnings about mutable class attributes (dependencies, operations) are a common false positive on Django migrations; I’d generally prefer configuring Ruff to ignore this rule for migrations/ rather than modifying standard Django migration patterns.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bcc68ca and 61a91c7.

📒 Files selected for processing (1)
  • commcare_connect/opportunity/migrations/0087_currency_country_opportunity_currency_fk.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
commcare_connect/opportunity/migrations/0087_currency_country_opportunity_currency_fk.py (3)
commcare_connect/program/utils.py (1)
  • populate_currency_and_country_fk_for_model (26-70)
commcare_connect/opportunity/models.py (2)
  • Currency (61-64)
  • Country (67-70)
commcare_connect/program/migrations/0003_program_currency_fk.py (1)
  • Migration (13-39)
🪛 Ruff (0.14.6)
commcare_connect/opportunity/migrations/0087_currency_country_opportunity_currency_fk.py

433-433: Unused function argument: schema_editor

(ARG001)


468-468: Unused function argument: schema_editor

(ARG001)


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

(RUF012)


477-525: 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

Copy link
Collaborator

@calellowitz calellowitz left a comment

Choose a reason for hiding this comment

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

offlined with @sravfeyn, fine to go forward as is

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 (1)
commcare_connect/opportunity/models.py (1)

61-65: LGTM! Currency model structure is correct.

The model correctly uses ISO 4217 3-letter codes as the primary key and includes an is_valid flag to handle invalid currency codes as described in the PR objectives (14 invalid currencies marked with is_valid=False).

Optionally, consider adding a __str__ method for better representation in Django admin and debugging:

 class Currency(models.Model):
     code = models.CharField(max_length=3, primary_key=True)  # ISO 4217
     name = models.CharField(max_length=64)
     is_valid = models.BooleanField(default=True)
+
+    def __str__(self):
+        return f"{self.code} - {self.name}"
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 61a91c7 and 213bf43.

📒 Files selected for processing (4)
  • commcare_connect/opportunity/migrations/0088_currency_country_opportunity_currency_fk.py (1 hunks)
  • commcare_connect/opportunity/models.py (2 hunks)
  • commcare_connect/program/migrations/0004_program_currency_fk.py (1 hunks)
  • commcare_connect/program/models.py (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
commcare_connect/program/models.py (1)
commcare_connect/opportunity/models.py (4)
  • Country (67-70)
  • Currency (61-64)
  • DeliveryType (52-58)
  • Opportunity (73-226)
commcare_connect/opportunity/migrations/0088_currency_country_opportunity_currency_fk.py (3)
commcare_connect/program/utils.py (1)
  • populate_currency_and_country_fk_for_model (26-70)
commcare_connect/opportunity/models.py (2)
  • Currency (61-64)
  • Country (67-70)
commcare_connect/program/migrations/0004_program_currency_fk.py (1)
  • Migration (13-39)
🪛 Ruff (0.14.8)
commcare_connect/opportunity/migrations/0088_currency_country_opportunity_currency_fk.py

433-433: Unused function argument: schema_editor

(ARG001)


468-468: Unused function argument: schema_editor

(ARG001)


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

(RUF012)


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

(RUF012)

commcare_connect/program/migrations/0004_program_currency_fk.py

9-9: Unused function argument: schema_editor

(ARG001)


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

(RUF012)


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

(RUF012)

🔇 Additional comments (11)
commcare_connect/opportunity/models.py (2)

67-71: LGTM! Country model structure is correct.

The model correctly uses ISO 3166-1 alpha-3 codes as the primary key. The nullable currency field with on_delete=SET_NULL is appropriate, as some countries have no official currency (e.g., Antarctica, State of Palestine per the COUNTRIES data in the migration).


100-101: LGTM! Foreign key additions are correct.

The new currency_fk and country foreign keys appropriately use on_delete=PROTECT to maintain referential integrity by preventing deletion of Currency or Country records that are in use. The nullable fields allow for gradual migration of existing data.

commcare_connect/program/models.py (2)

4-4: LGTM! Import additions are correct.

The imports of Country and Currency are necessary for the new foreign key fields added to the Program model.


16-17: LGTM! Program model changes mirror Opportunity model correctly.

The new currency_fk and country foreign keys are consistent with the Opportunity model changes, using on_delete=PROTECT for referential integrity and null=True for gradual data migration.

commcare_connect/program/migrations/0004_program_currency_fk.py (3)

9-10: LGTM! Data migration function correctly delegates to shared utility.

The function appropriately delegates to the shared populate_currency_and_country_fk_for_model utility with correct parameters for the Program model.


14-17: LGTM! Migration dependencies are correct.

The dependencies correctly ensure that:

  1. Currency and Country models exist (from opportunity 0088)
  2. Previous Program migration has run (program 0003)

19-39: LGTM! Migration operations are correctly structured.

The operations correctly:

  1. Add the currency_fk and country foreign key fields with appropriate constraints
  2. Populate the new fields using the shared utility function
  3. Use reverse_code=noop and run_on_secondary=False hints appropriately for data migrations
commcare_connect/opportunity/migrations/0088_currency_country_opportunity_currency_fk.py (4)

12-430: LGTM! ISO data structures are correctly formatted.

The COUNTRIES and CURRENCIES data structures are properly formatted as tuples sourced from ISO standards. Countries without official currencies correctly have None for the currency field (e.g., Antarctica, South Georgia, State of Palestine).


433-465: LGTM! Data loading function correctly creates Currency and Country records.

The function properly:

  1. Creates Currency records with ignore_conflicts=True for idempotency
  2. Builds a lookup dictionary to link countries to currencies
  3. Creates Country records with appropriate currency references
  4. Uses ignore_conflicts=True for Country creation (as addressed in past reviews)

468-470: LGTM! Data migration function correctly delegates to shared utility.

The function appropriately delegates to the shared populate_currency_and_country_fk_for_model utility with correct parameters for the Opportunity model, consistent with the Program migration.


472-525: LGTM! Migration operations are correctly ordered and structured.

The migration operations follow the correct sequence:

  1. Create Currency and Country models
  2. Add foreign key fields to Opportunity
  3. Load reference data (Currency and Country records)
  4. Populate foreign keys on existing Opportunity records

The use of reverse_code=noop and run_on_secondary=False hints is appropriate for data migrations.

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 (2)
commcare_connect/opportunity/tests/test_models.py (1)

1-4: Migration population test covers key scenarios well

This test exercises the important paths (valid, single-country, multi-country, and invalid currencies) and asserts both FK values and row counts, which should catch regressions in the migration logic. The mixed use of OpportunityFactory and django_apps.get_model is reasonable here for driving the same table via current models and the migration-style API.

If you want to make the invalid-currency assertion slightly more robust against future fixtures/seeding, you could also assert that invalid_currency_code does not exist before calling populate_currency_and_country_fk, but that’s optional and not a blocker.

Also applies to: 130-185

commcare_connect/opportunity/models.py (1)

62-72: Currency/Country model design and new FKs look sound; consider form-opt-in and display helpers

The normalized models and PROTECT FKs on Opportunity line up well with the migration and data model you described; I don’t see correctness issues here.

Two optional improvements you might consider:

  • For Opportunity.currency_fk and Opportunity.country, they’re null=True and populated via migration/logic; if these ever appear on admin or ModelForms, adding blank=True will better reflect that they’re optional at the form/validation layer too.

  • currency_fk = models.ForeignKey(Currency, on_delete=models.PROTECT, null=True)

  • country = models.ForeignKey(Country, on_delete=models.PROTECT, null=True)

  • currency_fk = models.ForeignKey(Currency, on_delete=models.PROTECT, null=True, blank=True)
  • country = models.ForeignKey(Country, on_delete=models.PROTECT, null=True, blank=True)

- Adding simple `__str__` implementations for `Currency` and `Country` (e.g., `return self.code` or `f"{self.code} - {self.name}"`) would make them much easier to work with in the Django admin and shell.

Both are non-blocking and can be deferred if you prefer.





Also applies to: 100-102

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used**: CodeRabbit UI

**Review profile**: CHILL

**Plan**: Pro

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 213bf43b10f8c2d76b42f6e52f80e0b99edf3234 and 7f901d626732c90acdac746ec5e95ef334d89d0f.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `commcare_connect/opportunity/models.py` (2 hunks)
* `commcare_connect/opportunity/tests/test_models.py` (3 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code graph analysis (1)</summary>

<details>
<summary>commcare_connect/opportunity/tests/test_models.py (2)</summary><blockquote>

<details>
<summary>commcare_connect/opportunity/tests/factories.py (1)</summary>

* `OpportunityFactory` (47-64)

</details>
<details>
<summary>commcare_connect/opportunity/migrations/0088_currency_country_opportunity_currency_fk.py (1)</summary>

* `populate_currency_and_country_fk` (468-469)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ 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)</summary>

* GitHub Check: pytest

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

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 (1)
commcare_connect/opportunity/tests/test_models.py (1)

144-144: Test coverage gap: Verify that INR (single-country currency) sets the country field.

The variable valid_currency_only = "INR" was flagged in a previous review as misleading because INR maps to a single country (India) according to the COUNTRIES data in the migration. While the previous comment was marked as resolved, the test still doesn't verify that the country field is populated for INR, unlike the assertion for KES at lines 173-174.

If INR is indeed a single-country currency, the test should verify that both currency_fk and country are set, similar to the KES test case.

🔎 Suggested enhancement to verify country is set for INR
+    # INR should also set country since it maps to a single country (India)
+    assert opp_valid.country_id == CountryModel.objects.get(code="IND").pk

Optionally, consider renaming the variable to better reflect its purpose:

-    valid_currency_only = "INR"
+    single_country_currency_inr = "INR"  # India only

Then update line 146 and 152 accordingly:

-    assert CurrencyModel.objects.filter(code=valid_currency_only).exists()
+    assert CurrencyModel.objects.filter(code=single_country_currency_inr).exists()
-    opp_valid = OpportunityFactory(currency=valid_currency_only, currency_fk=None, country=None)
+    opp_valid = OpportunityFactory(currency=single_country_currency_inr, currency_fk=None, country=None)
-    assert opp_valid.currency_fk_id == valid_currency_only
+    assert opp_valid.currency_fk_id == single_country_currency_inr
+    assert opp_valid.country_id == CountryModel.objects.get(code="IND").pk

Also applies to: 169-171

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7f901d6 and 651a83a.

📒 Files selected for processing (3)
  • commcare_connect/opportunity/models.py
  • commcare_connect/opportunity/tests/test_models.py
  • commcare_connect/program/migrations/0004_program_currency_fk.py
🧰 Additional context used
🧬 Code graph analysis (2)
commcare_connect/opportunity/tests/test_models.py (1)
commcare_connect/opportunity/tests/factories.py (1)
  • OpportunityFactory (47-64)
commcare_connect/program/migrations/0004_program_currency_fk.py (1)
commcare_connect/program/utils.py (1)
  • populate_currency_and_country_fk_for_model (26-70)
🪛 Ruff (0.14.8)
commcare_connect/program/migrations/0004_program_currency_fk.py

9-9: Unused function argument: schema_editor

(ARG001)


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

(RUF012)


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

(RUF012)

🔇 Additional comments (4)
commcare_connect/opportunity/tests/test_models.py (1)

130-186: Well-structured migration test with comprehensive scenario coverage.

The test effectively validates the migration logic across multiple scenarios: valid single-country currencies, multi-country currencies, and invalid currency codes. The use of importlib to dynamically load the migration function and the verification of counts before and after migration demonstrate thorough testing practices.

commcare_connect/opportunity/models.py (2)

62-72: Well-designed Currency and Country models following ISO standards.

The models correctly implement:

  • ISO 4217 currency codes (3-character primary keys)
  • ISO 3166-1 alpha-3 country codes (3-character primary keys)
  • is_valid flag on Currency to track placeholder/invalid currencies created during migration
  • Nullable currency FK on Country with SET_NULL to handle countries without official currencies

This design provides a solid foundation for normalizing currency and country data.


101-102: LGTM: Appropriate use of PROTECT for referential integrity.

The use of on_delete=models.PROTECT for both currency_fk and country ensures that Currency and Country reference data cannot be accidentally deleted while in use. The nullable design allows for backward compatibility during migration from the existing currency CharField.

commcare_connect/program/migrations/0004_program_currency_fk.py (1)

1-39: Well-structured migration with proper dependency ordering and shared utility reuse.

The migration correctly:

  • Adds currency_fk and country foreign keys to the Program model
  • Uses on_delete=PROTECT consistently with the Opportunity model
  • Reuses the shared utility populate_currency_and_country_fk_for_model for data migration
  • Declares dependencies on the opportunity migration that creates Currency and Country models
  • Sets run_on_secondary=False to prevent execution on read replicas

Note: The static analysis hints about unused schema_editor and ClassVar annotations are false positives for Django migration patterns and can be safely ignored.

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: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 651a83a and e9038b3.

📒 Files selected for processing (1)
  • commcare_connect/opportunity/migrations/0092_currency_country_opportunity_currency_fk.py
🧰 Additional context used
🧬 Code graph analysis (1)
commcare_connect/opportunity/migrations/0092_currency_country_opportunity_currency_fk.py (2)
commcare_connect/program/utils.py (1)
  • populate_currency_and_country_fk_for_model (26-70)
commcare_connect/opportunity/models.py (2)
  • Currency (62-65)
  • Country (68-71)
🪛 Ruff (0.14.8)
commcare_connect/opportunity/migrations/0092_currency_country_opportunity_currency_fk.py

433-433: Unused function argument: schema_editor

(ARG001)


468-468: Unused function argument: schema_editor

(ARG001)


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

(RUF012)


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

(RUF012)

🔇 Additional comments (5)
commcare_connect/opportunity/migrations/0092_currency_country_opportunity_currency_fk.py (5)

1-6: LGTM!

The imports are appropriate for this migration. Good use of the shared utility function from commcare_connect.program.utils to avoid code duplication.


12-263: LGTM!

The country data is well-structured and correctly sourced from ISO standards. The None values for currencies (Antarctica, South Georgia, Palestine) are appropriate for territories without official currencies.


433-464: LGTM!

The data loading logic is well-structured:

  • Creates currencies first with ignore_conflicts=True for safe re-runs
  • Efficiently builds lookup dictionary
  • Correctly handles countries without currencies (returns None from .get())
  • Uses bulk operations for performance

The unused schema_editor parameter flagged by static analysis is a false positive—it's required by Django's migration signature.


468-470: LGTM!

Good delegation to the shared utility function for FK population. This promotes code reuse and maintains consistency with the Program model migration.

The unused schema_editor parameter flagged by static analysis is a false positive—it's required by Django's migration signature.


472-525: LGTM!

The migration structure is correct:

  • Model definitions match the final state in models.py
  • Proper sequencing: create models → add fields → load data → populate FKs
  • ignore_conflicts=True enables safe re-runs
  • noop reverse is acceptable for data migrations
  • run_on_secondary=False correctly prevents redundant execution on replica databases

The on_delete policies establish appropriate data protection:

  • PROTECT on Opportunity FKs prevents accidental deletion of currencies/countries in use
  • SET_NULL on Country.currency allows reference data maintenance when not constrained by operational data

The ClassVar annotations flagged by static analysis are false positives for Django migration classes.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
commcare_connect/opportunity/tests/test_models.py (1)

169-170: Incomplete assertion: INR is a single-country currency that should also set country.

The test verifies that opp_valid.currency_fk_id is set to "INR" but doesn't check whether the country field is populated. According to the COUNTRIES data in the migration (line 119), INR maps to a single country ("IND", "India", "INR"), so the country field should also be set, similar to how KES is tested at lines 173-174.

🔎 Proposed fix to add country assertion for INR
     # valid currency should map to fk
     assert opp_valid.currency_fk_id == valid_currency_only
+    # INR is single-country (India), should also set country
+    assert opp_valid.country_id == CountryModel.objects.get(code="IND").pk
🧹 Nitpick comments (2)
commcare_connect/program/utils.py (2)

47-47: Remove unused noqa directive.

The noqa: E203 directive is unused since Ruff does not enable the E203 rule by default.

🔎 Proposed fix
-        batch = list(qs[start : start + BATCH_SIZE])  # noqa: E203
+        batch = list(qs[start : start + BATCH_SIZE])

55-60: Redundant dictionary assignment for invalid currency codes.

When an invalid currency code is encountered, line 58 reassigns the USD currency object back to the code_to_currency dictionary using code_to_currency[currency_obj.code] = currency_obj. Since currency_obj is retrieved from code_to_currency["USD"], USD should already exist in the dictionary, making this assignment unnecessary.

🔎 Proposed fix
             if raw_code not in code_to_currency:
                 # Set to USD when code is incorrect
                 currency_obj = code_to_currency["USD"]
-                code_to_currency[currency_obj.code] = currency_obj
             else:
                 currency_obj = code_to_currency[raw_code]
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3af1e20 and d054ccc.

📒 Files selected for processing (4)
  • commcare_connect/opportunity/migrations/0092_currency_country_opportunity_currency_fk.py
  • commcare_connect/opportunity/models.py
  • commcare_connect/opportunity/tests/test_models.py
  • commcare_connect/program/utils.py
🧰 Additional context used
🧬 Code graph analysis (2)
commcare_connect/program/utils.py (1)
commcare_connect/opportunity/models.py (2)
  • Currency (62-64)
  • Country (67-70)
commcare_connect/opportunity/migrations/0092_currency_country_opportunity_currency_fk.py (2)
commcare_connect/program/utils.py (1)
  • populate_currency_and_country_fk_for_model (26-68)
commcare_connect/opportunity/models.py (2)
  • Currency (62-64)
  • Country (67-70)
🪛 Ruff (0.14.8)
commcare_connect/program/utils.py

47-47: Unused noqa directive (non-enabled: E203)

Remove unused noqa directive

(RUF100)

commcare_connect/opportunity/migrations/0092_currency_country_opportunity_currency_fk.py

432-432: Unused function argument: schema_editor

(ARG001)


466-466: Unused function argument: schema_editor

(ARG001)


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

(RUF012)


475-522: 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 (8)
commcare_connect/program/utils.py (1)

26-44: LGTM: Function setup and lookup logic is correct.

The function signature, model retrieval, and dictionary construction for currency/country lookups are well-structured for efficient batch processing.

commcare_connect/opportunity/tests/test_models.py (1)

130-149: LGTM: Test setup correctly loads migration and verifies data.

The test properly uses importlib to dynamically load the migration module and retrieves models via django_apps. The currency/country setup assertions ensure the test data is properly initialized.

commcare_connect/opportunity/models.py (2)

62-71: LGTM: Currency and Country models are well-defined.

The models follow ISO standards (ISO 4217 for currencies, ISO 3166-1 alpha-3 for countries). Using on_delete=SET_NULL for Country.currency is appropriate since some countries have no official currency.


100-101: LGTM: Foreign key constraints use appropriate on_delete behavior.

Using on_delete=PROTECT for both currency_fk and country prevents accidental deletion of Currency or Country records that are referenced by Opportunity records, maintaining referential integrity as discussed in prior reviews.

commcare_connect/opportunity/migrations/0092_currency_country_opportunity_currency_fk.py (4)

9-263: LGTM: COUNTRIES data is comprehensive and properly sourced.

The country data follows ISO 3166-1 alpha-3 standards and correctly handles countries without official currencies by using None. The data source attribution is helpful for future maintenance.


266-429: LGTM: CURRENCIES data is comprehensive and follows ISO 4217.

The currency data is properly sourced from ISO standards and covers a comprehensive set of global currencies. The duplicate entry previously flagged has been resolved.


432-462: LGTM: Data loading function is efficient and idempotent.

The function uses bulk_create with ignore_conflicts=True for efficient, idempotent data loading. The lookup dictionary approach correctly handles the Currency-Country relationships, including countries without official currencies.

Note: The Ruff warning about unused schema_editor parameter is a false positive—Django migration functions require this signature.


466-522: LGTM: Migration structure is correct and properly sequenced.

The migration correctly:

  • Delegates to the shared utility function for FK population
  • Sequences operations to create models before adding fields
  • Uses RunPython.noop for reverse operations (appropriate for data migrations)
  • Sets run_on_secondary=False to prevent duplicate execution on database replicas

Note: The Ruff warnings about unused schema_editor and ClassVar annotations are false positives—these are standard Django migration patterns.

@sravfeyn
Copy link
Member Author

@ajeety4 Most of this code is already approved. Cal and I offlined about a small change - change the currency update strategy to default to USD currency for invalid currencies. I have updated the code per that d054ccc. Could you please review it?

Replace currency field with currency_fk
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

Caution

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

⚠️ Outside diff range comments (2)
commcare_connect/opportunity/forms.py (2)

1629-1629: Update to use currency_fk instead of currency field.

Line 1629 references self.opportunity.currency, but the model now uses currency_fk. This should be updated to self.opportunity.currency_fk.code if self.opportunity.currency_fk else "".

🔎 Proposed fix
-                label=f"Amount ({self.opportunity.currency})",
+                label=f"Amount ({self.opportunity.currency_fk.code if self.opportunity.currency_fk else ''})",

1684-1684: Update to use currency_fk instead of currency field.

Lines 1684 and 1696 reference self.opportunity.currency, but the model now uses currency_fk. These should be updated to use self.opportunity.currency_fk.code.

🔎 Proposed fix
-            exchange_rate = ExchangeRate.latest_exchange_rate(self.opportunity.currency, date)
+            exchange_rate = ExchangeRate.latest_exchange_rate(self.opportunity.currency_fk.code if self.opportunity.currency_fk else None, date)
             if not exchange_rate:
                 raise ValidationError("Exchange rate not available for selected date.")

             cleaned_data["exchange_rate"] = exchange_rate
             cleaned_data["amount_usd"] = round(amount / exchange_rate.rate, 2)

             cleaned_data["title"] = None
             cleaned_data["start_date"] = None
             cleaned_data["end_date"] = None
             cleaned_data["notes"] = None

-            exchange_rate = ExchangeRate.latest_exchange_rate(self.opportunity.currency, date)
+            # This line appears to be a duplicate - consider removing if already handled above
+            exchange_rate = ExchangeRate.latest_exchange_rate(self.opportunity.currency_fk.code if self.opportunity.currency_fk else None, date)

Also applies to: 1696-1696

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d054ccc and 23b6d27.

📒 Files selected for processing (9)
  • commcare_connect/opportunity/forms.py
  • commcare_connect/opportunity/models.py
  • commcare_connect/opportunity/tests/factories.py
  • commcare_connect/opportunity/tests/test_forms.py
  • commcare_connect/opportunity/tests/test_models.py
  • commcare_connect/program/forms.py
  • commcare_connect/program/tests/factories.py
  • commcare_connect/program/tests/test_forms.py
  • commcare_connect/program/tests/test_views.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • commcare_connect/opportunity/models.py
🧰 Additional context used
🧬 Code graph analysis (7)
commcare_connect/opportunity/tests/factories.py (1)
commcare_connect/opportunity/models.py (1)
  • Currency (62-67)
commcare_connect/program/tests/factories.py (1)
commcare_connect/opportunity/models.py (1)
  • Currency (62-67)
commcare_connect/opportunity/tests/test_models.py (2)
commcare_connect/opportunity/tests/factories.py (1)
  • OpportunityFactory (47-64)
commcare_connect/opportunity/migrations/0092_currency_country_opportunity_currency_fk.py (1)
  • populate_currency_and_country_fk (466-467)
commcare_connect/program/forms.py (2)
commcare_connect/opportunity/models.py (2)
  • Country (70-76)
  • Currency (62-67)
commcare_connect/program/models.py (1)
  • Program (9-28)
commcare_connect/program/tests/test_forms.py (1)
commcare_connect/program/forms.py (1)
  • ProgramForm (13-87)
commcare_connect/program/tests/test_views.py (1)
commcare_connect/opportunity/models.py (1)
  • Currency (62-67)
commcare_connect/opportunity/forms.py (1)
commcare_connect/opportunity/models.py (16)
  • Country (70-76)
  • Currency (62-67)
  • Meta (266-267)
  • Meta (284-289)
  • Meta (390-395)
  • Meta (470-473)
  • Meta (511-512)
  • Meta (576-577)
  • Meta (775-783)
  • Meta (800-803)
  • Meta (842-846)
  • Meta (886-887)
  • Meta (900-901)
  • Opportunity (79-232)
  • ExchangeRate (464-484)
  • latest_exchange_rate (476-484)
⏰ 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 (10)
commcare_connect/opportunity/tests/factories.py (1)

3-3: LGTM! Factory updated correctly to use currency_fk.

The factory appropriately uses LazyFunction to fetch the Currency object dynamically. This ensures the Currency record exists before attempting to create an Opportunity.

Also applies to: 7-7, 60-60

commcare_connect/program/tests/factories.py (1)

1-1: LGTM! Factory correctly updated to use currency_fk.

The changes align with the new Currency model and use LazyFunction appropriately to fetch the Currency object at factory build time.

Also applies to: 4-4, 15-15

commcare_connect/opportunity/tests/test_forms.py (1)

61-61: LGTM! Tests correctly updated to use currency_fk.

The test updates consistently reference currency_fk and access the code via currency_fk.code where needed. The changes align well with the new Currency model structure.

Also applies to: 77-77, 90-90, 103-103, 196-199, 302-302, 310-310, 367-367

commcare_connect/program/forms.py (2)

14-25: LGTM! Currency and Country fields properly added.

The new currency_fk and country fields are correctly defined as ModelChoiceFields with appropriate querysets, widgets, and labels.


97-102: LGTM! Managed opportunities correctly inherit program currency and country.

The loop correctly sets both currency_fk and country fields to readonly and disabled, ensuring managed opportunities use the program's values.

commcare_connect/opportunity/tests/test_models.py (1)

130-186: LGTM! Comprehensive migration test with good coverage.

The test thoroughly exercises the currency and country FK population logic, covering:

  • Valid currency codes mapping to currency_fk
  • Single-country currencies auto-setting country FK
  • Multi-country currencies leaving country as NULL
  • Invalid currencies defaulting to USD without creating new records

The test correctly verifies that Currency and Country counts remain unchanged, confirming no spurious records are created.

commcare_connect/opportunity/forms.py (2)

112-123: LGTM! Currency and Country fields properly added to forms.

The currency_fk and country fields are correctly defined as ModelChoiceFields with appropriate querysets, widgets, and labels for both OpportunityChangeForm and OpportunityInitForm.

Also applies to: 316-327


1019-1022: LGTM! Correctly uses currency_fk.code for label.

The code safely accesses the currency code via currency_fk.code with appropriate null checking.

commcare_connect/program/tests/test_views.py (1)

47-47: The currency_fk field in ProgramForm is a ModelChoiceField that by default uses the model's primary key for validation. ModelChoiceField's to_python method uses self.to_field_name or 'pk' to query the queryset. The Currency model defines code as its primary key (code = models.CharField(max_length=3, primary_key=True)), so passing the string "USD" at line 47 is correct and consistent with line 73—both provide the primary key value. No changes needed.

commcare_connect/program/tests/test_forms.py (1)

40-40: No action needed—test data is consistent.

The currency_fk field correctly uses string codes ("USD", "EUR") because the Currency model defines code as the primary key. ModelChoiceField expects primary keys by default, so passing "USD" is the correct format. Both test_forms.py line 40 and test_views.py line 73 use the code string (the primary key) consistently, confirming the test data is appropriate.

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

Caution

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

⚠️ Outside diff range comments (1)
commcare_connect/opportunity/forms.py (1)

1681-1702: Use currency_fk consistently and remove duplicate code.

Lines 1681 and 1693 reference the old currency field instead of the new currency_fk.code pattern used elsewhere in this file. Additionally, the exchange rate calculation logic is duplicated in lines 1681-1686 and 1693-1702.

This creates several issues:

  • Inconsistency: Line 1626 in the same form uses currency_code, while lines 1681 and 1693 use the old currency field
  • Inconsistency with other forms: PaymentInvoiceForm.clean() at line 1450 uses currency_fk.code
  • Potential breakage: If the old currency CharField is deprecated or removed, this code will fail
  • Missing null safety: Unlike line 1450, there's no defensive check for currency_fk existence
🔎 Proposed fix to use currency_fk and eliminate duplication
     def clean(self):
         cleaned_data = super().clean()
         amount = cleaned_data.get("amount")
         date = cleaned_data.get("date")
 
         if amount is None or date is None:
             return cleaned_data  # Let individual field errors handle missing values
 
         if not self.is_service_delivery:
-            exchange_rate = ExchangeRate.latest_exchange_rate(self.opportunity.currency, date)
+            currency = getattr(self.opportunity, "currency_fk", None)
+            exchange_rate = ExchangeRate.latest_exchange_rate(currency.code if currency else None, date)
             if not exchange_rate:
                 raise ValidationError("Exchange rate not available for selected date.")
 
             cleaned_data["exchange_rate"] = exchange_rate
-            cleaned_data["amount_usd"] = round(amount / exchange_rate.rate, 2)
 
             cleaned_data["title"] = None
             cleaned_data["start_date"] = None
             cleaned_data["end_date"] = None
             cleaned_data["notes"] = None
 
-            exchange_rate = ExchangeRate.latest_exchange_rate(self.opportunity.currency, date)
-            if not exchange_rate:
-                raise ValidationError("Exchange rate not available for selected date.")
-
             if cleaned_data.get("usd_currency"):
                 cleaned_data["amount_usd"] = amount
                 cleaned_data["amount"] = round(amount * exchange_rate.rate, 2)
             else:
                 cleaned_data["amount"] = amount
                 cleaned_data["amount_usd"] = round(amount / exchange_rate.rate, 2)
         else:
             start_date = cleaned_data.get("start_date")
             end_date = cleaned_data.get("end_date")
 
             if not start_date:
                 raise ValidationError({"start_date": "Start date is required for service delivery invoices."})
             if not end_date:
                 raise ValidationError({"end_date": "End date is required for service delivery invoices."})
 
             if end_date < start_date:
                 raise ValidationError({"end_date": "End date cannot be earlier than start date."})
 
         return cleaned_data
🧹 Nitpick comments (14)
commcare_connect/opportunity/tests/test_completed_work.py (1)

143-144: Same as lines 83-84: Consider more explicit FK assignment.

Same concern as the previous test method. Assigning "EUR" directly to the ForeignKey currency_fk is implicit. Use currency_fk_id = "EUR" or fetch the Currency instance for clarity and reliability.

🔎 Proposed refactor
-        opp_access.opportunity.currency_fk = "EUR"
+        opp_access.opportunity.currency_fk_id = "EUR"
         opp_access.opportunity.save()
commcare_connect/opportunity/tasks.py (1)

293-296: Add fallback for currency_code in notification message.

If currency_code returns None (when currency_fk is not set), the notification message will display "None" instead of a currency code, which could confuse users.

🔎 Proposed fix
                    "body": gettext(
                        "You have received a payment of "
-                       f"{opportunity.currency_code} {payment.amount} for {opportunity.name}.",
+                       f"{opportunity.currency_code or ''} {payment.amount} for {opportunity.name}.",
                    ),
commcare_connect/templates/opportunity/user_visit_verification.html (2)

134-134: Add fallback for currency_code in label.

If currency_code returns None, the label will display "Accrued Amount (None)" which looks unprofessional. Consider adding a default filter.

🔎 Proposed fix
-            <span>Accrued Amount ({{ opportunity.currency_code }})</span>
+            <span>Accrued Amount ({{ opportunity.currency_code|default:'' }})</span>

147-147: Add fallback for currency_code in label.

If currency_code returns None, the label will display "Paid Amount (None)" which looks unprofessional. Consider adding a default filter.

🔎 Proposed fix
-            <span>Paid Amount ({{ opportunity.currency_code }})</span>
+            <span>Paid Amount ({{ opportunity.currency_code|default:'' }})</span>
commcare_connect/templates/opportunity/email/invoice_paid.txt (1)

9-9: Add fallback for currency_code in email template.

If currency_code returns None, the email will display "None" as the currency, which could confuse recipients. Since this is a plain text template, consider using a filter or conditional logic to provide a fallback.

🔎 Proposed fix
-Amount Paid: {{opportunity.currency_code}} {{ invoice.amount }}
+Amount Paid: {{opportunity.currency_code|default:''}} {{ invoice.amount }}
commcare_connect/opportunity/tables.py (3)

414-414: Add fallback for currency_code in column header.

If currency_code returns None, the column header will display "Amount (None)" which looks unprofessional.

🔎 Proposed fix
-        self.base_columns["amount"].verbose_name = f"Amount ({self.opportunity.currency_code})"
+        self.base_columns["amount"].verbose_name = f"Amount ({self.opportunity.currency_code or ''})"

647-654: Add fallback for currency_code in rendered value.

If currency_code returns None, the cell will display "None" before the amount, which looks unprofessional.

🔎 Proposed fix
     def render_payments_due(self, value, record):
         if value is None:
             value = 0
 
-        value = f"{record.currency_code} {intcomma(value)}"
+        value = f"{record.currency_code or ''} {intcomma(value)}"
         return self.render_worker_list_url_column(
             value=value, opp_id=record.id, url_slug="worker_payments", sort="sort=-total_paid"
         )

735-740: Add fallback for currency_code in rendered value.

If currency_code returns None, the cell will display "None" before the amount, which looks unprofessional.

🔎 Proposed fix
     def render_worker_earnings(self, value, record):
         url = reverse("opportunity:worker_payments", args=(self.org_slug, record.id))
         url += "?sort=-payment_accrued"
-        value = f"{record.currency_code} {intcomma(value)}"
+        value = f"{record.currency_code or ''} {intcomma(value)}"
         value = format_html('<a href="{}">{}</a>', url, value)
         return self._render_div(value, extra_classes=self.stats_style)
commcare_connect/templates/components/worker_page/payment_history.html (1)

35-35: Add fallback for currency_code in confirmation message.

If currency_code returns None, the confirmation message will display "None" as the currency, which could confuse users.

🔎 Proposed fix
-          Please confirm to rollback the payment of <b>{{ latest_payment.amount }} {{ opportunity.currency_code }}</b>
+          Please confirm to rollback the payment of <b>{{ latest_payment.amount }} {{ opportunity.currency_code|default:'' }}</b>
commcare_connect/templates/opportunity/email/invoice_paid.html (1)

14-14: Add fallback for currency_code in email template.

If currency_code returns None, the email will display "None" as the currency, which could confuse recipients.

🔎 Proposed fix
-    <li><strong>{% trans "Amount Paid" %}:</strong> {{ opportunity.currency_code }} {{ invoice.amount }}</li>
+    <li><strong>{% trans "Amount Paid" %}:</strong> {{ opportunity.currency_code|default:'' }} {{ invoice.amount }}</li>
commcare_connect/program/tests/test_views.py (1)

40-50: Program create/update tests correctly switched to FK-based currency fields

Using "currency_fk": "USD"/"INR" and "country": "USA"/"IND" matches the new FK definitions (PK is the 3‑letter code), and asserting self.program.currency_fk_id == data["currency_fk"] verifies the update path. Consider also asserting country_id if you want full coverage of the new field, but this is optional.

Also applies to: 67-85

commcare_connect/program/views.py (1)

329-345: Use select_related("currency_fk") to avoid per-row queries for currency_code

Switching to f"{data.currency_code} {data.pending_payment}" is correct with the new FK model, but without select_related("currency_fk") this can issue one extra query per opportunity. Consider:

Proposed optimization
pending_payments_data_opps = (
-    Opportunity.objects.filter(managed=True, organization=org)
+    Opportunity.objects.filter(managed=True, organization=org).select_related("currency_fk")
     .annotate(
         pending_payment=Sum("opportunityaccess__payment_accrued") - Sum("opportunityaccess__payment__amount")
     )
     .filter(pending_payment__gte=0)
)
commcare_connect/opportunity/views.py (1)

454-460: Consistent migration from currency to currency_code across opportunity views

All these call sites now correctly use request.opportunity.currency_code (or result.currency_code) for display, reporting, and table labeling, and pass the code into helpers (InvoiceLineItemsTable, InvoiceDeliveriesTable, payment_report, exchange_rate_preview) that expect a currency string. This aligns with the new FK-backed currency model without changing semantics.

If you want to polish UX for opportunities lacking a currency, you could conditionally omit the currency prefix (as you already do in amount_with_currency) wherever the code might be None, but it’s not required for correctness.

Also applies to: 667-675, 1295-1305, 1529-1538, 2610-2612, 2764-2787, 2813-2836, 2884-2887, 2920-2921

commcare_connect/conftest.py (1)

1-4: Autouse fixture for seeding Currency/Country data is sound

The ensure_currency_country_data fixture correctly reuses the 0092 migration’s load_currency_and_country_data function and guards on Currency.objects.exists(), so tests always have ISO currency/country rows without duplicating logic. The unused db parameter is intentional to activate the database; you can ignore Ruff’s ARG001 here or silence it with # noqa: ARG001 if it’s noisy.

Also applies to: 155-163

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 23b6d27 and e7f8832.

📒 Files selected for processing (20)
  • commcare_connect/conftest.py
  • commcare_connect/opportunity/forms.py
  • commcare_connect/opportunity/models.py
  • commcare_connect/opportunity/tables.py
  • commcare_connect/opportunity/tasks.py
  • commcare_connect/opportunity/tests/factories.py
  • commcare_connect/opportunity/tests/test_completed_work.py
  • commcare_connect/opportunity/utils/completed_work.py
  • commcare_connect/opportunity/views.py
  • commcare_connect/opportunity/visit_import.py
  • commcare_connect/program/models.py
  • commcare_connect/program/tests/factories.py
  • commcare_connect/program/tests/test_views.py
  • commcare_connect/program/views.py
  • commcare_connect/templates/components/worker_page/payment_history.html
  • commcare_connect/templates/opportunity/email/invoice_paid.html
  • commcare_connect/templates/opportunity/email/invoice_paid.txt
  • commcare_connect/templates/opportunity/invoice_payment_report.html
  • commcare_connect/templates/opportunity/user_visit_verification.html
  • commcare_connect/templates/program/pm_home.html
✅ Files skipped from review due to trivial changes (1)
  • commcare_connect/templates/opportunity/invoice_payment_report.html
🚧 Files skipped from review as they are similar to previous changes (2)
  • commcare_connect/program/tests/factories.py
  • commcare_connect/program/models.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-11T08:11:43.757Z
Learnt from: sravfeyn
Repo: dimagi/commcare-connect PR: 804
File: commcare_connect/opportunity/views.py:909-910
Timestamp: 2025-11-11T08:11:43.757Z
Learning: In commcare_connect/opportunity/views.py, when calling update_payment_accrued, it's acceptable and more efficient to pass a QuerySet (e.g., visits.values_list("user_id", flat=True).distinct()) rather than converting to a list, as the function only uses it in a filter(user__in=users) context where Django natively supports QuerySets.

Applied to files:

  • commcare_connect/opportunity/visit_import.py
🧬 Code graph analysis (9)
commcare_connect/opportunity/utils/completed_work.py (3)
commcare_connect/opportunity/visit_import.py (1)
  • get_exchange_rate (423-441)
commcare_connect/opportunity/models.py (1)
  • currency_code (119-123)
commcare_connect/program/models.py (1)
  • currency_code (31-35)
commcare_connect/opportunity/visit_import.py (2)
commcare_connect/opportunity/models.py (1)
  • currency_code (119-123)
commcare_connect/program/models.py (1)
  • currency_code (31-35)
commcare_connect/opportunity/tests/factories.py (2)
commcare_connect/commcarehq/tests/factories.py (1)
  • HQServerFactory (14-21)
commcare_connect/opportunity/models.py (3)
  • Country (70-76)
  • Currency (62-67)
  • VisitValidationStatus (462-468)
commcare_connect/opportunity/views.py (3)
commcare_connect/opportunity/models.py (3)
  • currency_code (119-123)
  • ExchangeRate (471-491)
  • latest_exchange_rate (483-491)
commcare_connect/program/models.py (1)
  • currency_code (31-35)
commcare_connect/opportunity/tables.py (2)
  • InvoiceLineItemsTable (1582-1611)
  • InvoiceDeliveriesTable (1614-1641)
commcare_connect/opportunity/tasks.py (2)
commcare_connect/opportunity/models.py (1)
  • currency_code (119-123)
commcare_connect/program/models.py (1)
  • currency_code (31-35)
commcare_connect/program/views.py (2)
commcare_connect/opportunity/models.py (1)
  • currency_code (119-123)
commcare_connect/program/models.py (1)
  • currency_code (31-35)
commcare_connect/opportunity/tables.py (2)
commcare_connect/opportunity/models.py (1)
  • currency_code (119-123)
commcare_connect/program/models.py (1)
  • currency_code (31-35)
commcare_connect/opportunity/models.py (1)
commcare_connect/program/models.py (1)
  • currency_code (31-35)
commcare_connect/conftest.py (2)
commcare_connect/opportunity/models.py (1)
  • Currency (62-67)
commcare_connect/opportunity/migrations/0092_currency_country_opportunity_currency_fk.py (1)
  • load_currency_and_country_data (432-462)
🪛 Ruff (0.14.8)
commcare_connect/opportunity/visit_import.py

318-318: Avoid specifying long messages outside the exception class

(TRY003)

commcare_connect/conftest.py

155-155: Unused function argument: db

(ARG001)

⏰ 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 (15)
commcare_connect/opportunity/tests/test_completed_work.py (1)

83-84: The code is correct and follows standard Django patterns. Django supports CharField primary keys on models, and ForeignKey fields can be assigned by the primary key value directly. The EUR currency is guaranteed to exist because migration 0092 bulk creates all currencies, and conftest.py's autouse fixture ensures they're loaded for all tests. Using currency_fk = "EUR" is standard and clear; the suggested alternatives are purely optional style preferences.

Likely an incorrect or invalid review comment.

commcare_connect/templates/program/pm_home.html (1)

93-93: LGTM: Currency display updated correctly.

The budget line now properly renders the currency using currency_code with a fallback to an empty string when the currency is not set.

commcare_connect/opportunity/utils/completed_work.py (2)

126-126: LGTM: Exchange rate calculation updated correctly.

The code now uses currency_code for exchange rate lookups. The get_exchange_rate function (shown in relevant code snippets) properly validates that currency_code is not None and raises an ImportException if it is, ensuring robust error handling.


224-226: LGTM: Exchange rate calculation updated correctly.

The code now uses currency_code for exchange rate lookups. The get_exchange_rate function properly validates that currency_code is not None and raises an ImportException if it is, ensuring robust error handling.

commcare_connect/opportunity/tables.py (1)

1139-1147: LGTM: Proper error handling for currency_code access.

The try-except block correctly handles potential IndexError and AttributeError when accessing currency_code, defaulting to an empty string. This defensive programming ensures the table renders gracefully even when data is missing.

commcare_connect/opportunity/visit_import.py (1)

302-320: Currency handling in payment import correctly migrated to currency_code

Using opportunity.currency_code for both the initial exchange_rate_today and dated lookups keeps behavior consistent with the new FK-backed model and with get_exchange_rate’s expectations. The updated error text (“Currency code … is invalid”) is also clearer and aligned with the new field.

No further changes needed here.

Also applies to: 392-403

commcare_connect/opportunity/tests/factories.py (1)

3-7: OpportunityFactory defaults correctly wired to Currency/Country

Using LazyFunction(lambda: Currency.objects.get(code="USD")) and Country.objects.get(code="USA") is a good way to keep factory defaults in sync with the seeded ISO data. This pairs cleanly with the ensure_currency_country_data fixture and keeps tests using OpportunityFactory on the new FK-backed currency/country model without leaking integer IDs.

Nothing blocking here.

Also applies to: 47-62

commcare_connect/opportunity/models.py (1)

62-77: New Currency/Country models and Opportunity FKs look correct

Currency/Country are modeled with sensible 3‑char PKs, Country.currency uses SET_NULL appropriately, and Opportunity.currency_fk/country use PROTECT, which matches earlier review guidance for referential integrity. The currency_code property is a simple, reusable accessor that downstream views/tests are already using consistently.

No issues from a modeling or API perspective.

Also applies to: 104-107, 118-123

commcare_connect/opportunity/forms.py (7)

21-23: LGTM!

The imports for Country and Currency models are correctly added.


112-123: LGTM!

The currency_fk and country ModelChoiceField definitions are correctly implemented with appropriate widgets and ordering.


131-132: LGTM!

The Meta fields and layout correctly reference the new currency_fk and country fields instead of the deprecated currency field.

Also applies to: 191-192


316-327: LGTM!

The OpportunityInitForm correctly implements the same currency_fk and country field pattern as OpportunityChangeForm.

Also applies to: 335-336, 369-370


588-590: LGTM!

The initial field population correctly includes currency_fk and country instead of the old currency field.


1019-1020: LGTM!

The conditional label update using currency_code property is defensively coded and handles the case when currency information might be unavailable.


1388-1388: LGTM!

The PaymentInvoiceForm uses defensive patterns with getattr and conditional access to safely retrieve currency information from currency_fk.

Also applies to: 1402-1402, 1449-1450

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 (1)
commcare_connect/program/tests/test_forms.py (1)

65-75: Consider renaming test to reflect actual validation.

The test name test_program_form_currency_length is now misleading since it validates that an invalid currency code (not a length constraint) is rejected. A more descriptive name would be test_program_form_invalid_currency.

🔎 Suggested rename
-    def test_program_form_currency_length(self, program_manager_org_user_admin, program_manager_org, delivery_type):
+    def test_program_form_invalid_currency(self, program_manager_org_user_admin, program_manager_org, delivery_type):
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e7f8832 and e105f41.

📒 Files selected for processing (6)
  • commcare_connect/opportunity/tests/test_completed_work.py
  • commcare_connect/opportunity/tests/test_forms.py
  • commcare_connect/opportunity/utils/completed_work.py
  • commcare_connect/program/tests/test_forms.py
  • commcare_connect/reports/helpers.py
  • commcare_connect/reports/tests/test_reports.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • commcare_connect/opportunity/utils/completed_work.py
  • commcare_connect/opportunity/tests/test_forms.py
  • commcare_connect/opportunity/tests/test_completed_work.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-11T08:11:43.757Z
Learnt from: sravfeyn
Repo: dimagi/commcare-connect PR: 804
File: commcare_connect/opportunity/views.py:909-910
Timestamp: 2025-11-11T08:11:43.757Z
Learning: In commcare_connect/opportunity/views.py, when calling update_payment_accrued, it's acceptable and more efficient to pass a QuerySet (e.g., visits.values_list("user_id", flat=True).distinct()) rather than converting to a list, as the function only uses it in a filter(user__in=users) context where Django natively supports QuerySets.

Applied to files:

  • commcare_connect/reports/helpers.py
🧬 Code graph analysis (1)
commcare_connect/program/tests/test_forms.py (1)
commcare_connect/program/forms.py (1)
  • ProgramForm (13-87)
⏰ 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)
commcare_connect/reports/helpers.py (1)

126-127: LGTM! Currency filter successfully migrated to FK-based approach.

The update to filter by currency_fk instead of the denormalized currency field is correct and consistent across both filter dictionaries. The Django ORM will properly handle the FK relationship.

commcare_connect/reports/tests/test_reports.py (1)

249-249: LGTM! Correct FK ID assignment in test factory.

The update to use opportunity__currency_fk_id correctly assigns the Currency FK by its primary key (ISO code). The test properly validates that currency-based filtering works as expected with the new FK relationship.

commcare_connect/program/tests/test_forms.py (4)

34-44: LGTM!

Test data correctly updated to use currency_fk and country fields with string code values, consistent with the ModelChoiceField configuration in the form.


77-91: LGTM!

The assertion correctly accesses currency_fk.code to compare against the string value submitted in the form data.


124-130: LGTM!

The test correctly verifies that currency_fk is initialized from the program and marked as readonly/disabled to prevent modification.


149-165: LGTM!

The test correctly validates that currency_fk is propagated from the program to the managed opportunity upon form save.

Copy link
Contributor

@ajeety4 ajeety4 left a comment

Choose a reason for hiding this comment

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

The latest change looks good to me

@sravfeyn sravfeyn merged commit 8687d9c into main Dec 24, 2025
7 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Jan 2, 2026
1 task
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.

6 participants