Skip to content

perf(spp_programs): batch create entitlements and payments#128

Open
kneckinator wants to merge 1 commit into19.0from
ken/optimize_spp_program_phase_5
Open

perf(spp_programs): batch create entitlements and payments#128
kneckinator wants to merge 1 commit into19.0from
ken/optimize_spp_program_phase_5

Conversation

@kneckinator
Copy link
Contributor

@kneckinator kneckinator commented Mar 20, 2026

Summary

  • Batch-create entitlements in SppCashEntitlementManager.prepare_entitlements() (single create(vals_list) instead of per-beneficiary loop)
  • Batch-create payments in DefaultFilePaymentManager._prepare_payments() (single create(vals_list) per batch tag instead of per-entitlement loop)
  • Add bank_ids prefetch to avoid N+1 queries during payment preparation

Changes

File Before After
entitlement_manager_cash.py:159-165 for ent: self.env["spp.entitlement"].create(dict) — one INSERT per beneficiary Collect all dicts, filter zero amounts, single create(vals_list)
payment_manager.py:214-249 for entitlement: self.env["spp.payment"].create(dict) — one INSERT per entitlement, plus per-record payment.account_number = write Build vals list with account_number included, single create(vals_list), then batch_payments.write({"batch_id": ...}) for batch assignment

Note: DefaultCashEntitlementManager.prepare_entitlements() in entitlement_manager_base.py already used batch create() — no change needed there.


Context

Phase 5 of 9 in the spp_programs performance optimization effort. Independent of other phases.


Test Plan

  • ./scripts/test_single_module.sh spp_programs passes
  • test_cash_manager_batch_creates_entitlements — verifies single create() call for 5 beneficiaries
  • test_payment_manager_batch_creates_payments — verifies single create() call for 5 entitlements

Cash entitlement manager now collects all entitlement dicts and calls
create() once instead of per-beneficiary. Payment manager collects all
payment dicts per batch tag and calls create() once, then batch-assigns
via write(). Also prefetches bank_ids to avoid N+1 queries.
@gemini-code-assist
Copy link

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly improves performance within the spp_programs module by optimizing database interactions during entitlement and payment generation. By converting several individual record creation operations into single batch operations and implementing strategic data prefetching, the changes aim to reduce the number of database queries, thereby enhancing the efficiency and scalability of these core processes. This is part of a broader effort to optimize the performance of the spp_programs module.

Highlights

  • Batch Entitlement Creation: Entitlement creation in SppCashEntitlementManager.prepare_entitlements() has been optimized to use a single batch create(vals_list) call, replacing the previous per-beneficiary loop.
  • Batch Payment Creation: Payment creation in DefaultFilePaymentManager._prepare_payments() now utilizes a single batch create(vals_list) per batch tag, moving away from the per-entitlement loop.
  • N+1 Query Optimization: Added bank_ids prefetching to DefaultFilePaymentManager._prepare_payments() to eliminate N+1 queries during the payment preparation process.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces significant performance improvements by batching the creation of entitlements and payments, replacing loops of single create calls with batch create(vals_list) operations. The changes are well-implemented and include new tests to verify the batching behavior. My review includes a few suggestions to further improve code readability by using more idiomatic Python constructs and to enhance the new tests for better coverage.

Comment on lines 161 to +166
for ent in new_entitlements_to_create:
initial_amount = new_entitlements_to_create[ent]["initial_amount"]
new_entitlements_to_create[ent]["initial_amount"] = self._check_subsidy(initial_amount)
# Create non-zero entitlements only
if new_entitlements_to_create[ent]["initial_amount"] > 0.0:
self.env["spp.entitlement"].create(new_entitlements_to_create[ent])
vals_list.append(new_entitlements_to_create[ent])

Choose a reason for hiding this comment

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

medium

While the logic is correct, the loop can be made more direct and Pythonic by iterating over the dictionary's values instead of its keys. This improves readability by removing the extra dictionary lookup new_entitlements_to_create[ent] in each iteration.

Suggested change
for ent in new_entitlements_to_create:
initial_amount = new_entitlements_to_create[ent]["initial_amount"]
new_entitlements_to_create[ent]["initial_amount"] = self._check_subsidy(initial_amount)
# Create non-zero entitlements only
if new_entitlements_to_create[ent]["initial_amount"] > 0.0:
self.env["spp.entitlement"].create(new_entitlements_to_create[ent])
vals_list.append(new_entitlements_to_create[ent])
for vals in new_entitlements_to_create.values():
initial_amount = vals["initial_amount"]
vals["initial_amount"] = self._check_subsidy(initial_amount)
# Create non-zero entitlements only
if vals["initial_amount"] > 0.0:
vals_list.append(vals)

Comment on lines +221 to 236
payment_vals_list = []
for entitlement_id in tag_entitlements:
account_number = None
if entitlement_id.partner_id.bank_ids:
account_number = entitlement_id.partner_id.bank_ids[0].acc_number
payment_vals_list.append(
{
"name": str(uuid4()),
"entitlement_id": entitlement_id.id,
"cycle_id": entitlement_id.cycle_id.id,
"amount_issued": entitlement_id.initial_amount,
"payment_fee": entitlement_id.transfer_fee,
"state": "issued",
"account_number": account_number,
}
)

Choose a reason for hiding this comment

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

medium

The logic for building payment_vals_list can be made more concise and Pythonic by using a list comprehension. This improves readability and reduces boilerplate code.

            payment_vals_list = [
                {
                    "name": str(uuid4()),
                    "entitlement_id": ent.id,
                    "cycle_id": ent.cycle_id.id,
                    "amount_issued": ent.initial_amount,
                    "payment_fee": ent.transfer_fee,
                    "state": "issued",
                    "account_number": ent.partner_id.bank_ids[0].acc_number if ent.partner_id.bank_ids else None,
                }
                for ent in tag_entitlements
            ]

Comment on lines +143 to +172
call_count = 0

def counting_create(self_model, vals_list):
nonlocal call_count
call_count += 1
return original_create(self_model, vals_list)

# Add a batch tag so we enter the loop
batch_tag = self.env["spp.payment.batch.tag"].create(
{
"name": "Test Tag",
"order": 1,
"domain": "[]",
"max_batch_size": 500,
}
)
self.payment_manager.batch_tag_ids = [(4, batch_tag.id)]

with patch.object(
type(self.env["spp.payment"]),
"create",
counting_create,
):
self.payment_manager._prepare_payments(self.cycle, self.entitlements)

self.assertEqual(
call_count,
1,
f"create() should be called once (batch), was called {call_count} times",
)

Choose a reason for hiding this comment

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

medium

For better test coverage and consistency with test_cash_manager_batch_creates_entitlements, this test should also verify the total number of records created, not just the number of create() calls. This ensures that the batch operation not only runs once but also processes the correct number of items.

        call_count = 0
        total_created = 0

        def counting_create(self_model, vals_list):
            nonlocal call_count, total_created
            call_count += 1
            if isinstance(vals_list, list):
                total_created += len(vals_list)
            else:
                total_created += 1
            return original_create(self_model, vals_list)

        # Add a batch tag so we enter the loop
        batch_tag = self.env["spp.payment.batch.tag"].create(
            {
                "name": "Test Tag",
                "order": 1,
                "domain": "[]",
                "max_batch_size": 500,
            }
        )
        self.payment_manager.batch_tag_ids = [(4, batch_tag.id)]

        with patch.object(
            type(self.env["spp.payment"]),
            "create",
            counting_create,
        ):
            self.payment_manager._prepare_payments(self.cycle, self.entitlements)

        self.assertEqual(
            call_count,
            1,
            f"create() should be called once (batch), was called {call_count} times",
        )
        self.assertEqual(total_created, 5)

@codecov
Copy link

codecov bot commented Mar 20, 2026

Codecov Report

❌ Patch coverage is 44.00000% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.98%. Comparing base (1a91296) to head (5ecee19).
⚠️ Report is 40 commits behind head on 19.0.

Files with missing lines Patch % Lines
spp_programs/models/managers/payment_manager.py 52.38% 10 Missing ⚠️
...ograms/models/managers/entitlement_manager_cash.py 0.00% 4 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             19.0     #128      +/-   ##
==========================================
- Coverage   70.14%   69.98%   -0.17%     
==========================================
  Files         739      783      +44     
  Lines       43997    46605    +2608     
==========================================
+ Hits        30863    32617    +1754     
- Misses      13134    13988     +854     
Flag Coverage Δ
spp_api_v2 79.96% <ø> (ø)
spp_api_v2_change_request 66.66% <ø> (ø)
spp_api_v2_cycles 71.12% <ø> (ø)
spp_api_v2_data 64.41% <ø> (ø)
spp_api_v2_entitlements 70.19% <ø> (ø)
spp_api_v2_gis 71.52% <ø> (ø)
spp_api_v2_products 66.27% <ø> (ø)
spp_api_v2_service_points 70.94% <ø> (ø)
spp_api_v2_simulation 71.12% <ø> (ø)
spp_api_v2_vocabulary 57.26% <ø> (ø)
spp_audit 64.19% <ø> (ø)
spp_base_common 90.26% <ø> (ø)
spp_case_entitlements 97.61% <ø> (ø)
spp_case_programs 97.14% <ø> (ø)
spp_cel_event 85.11% <ø> (?)
spp_claim_169 58.11% <ø> (?)
spp_dci_client_dr 55.87% <ø> (?)
spp_dci_client_ibr 60.17% <ø> (?)
spp_programs 45.82% <44.00%> (+0.31%) ⬆️
spp_security 66.66% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...ograms/models/managers/entitlement_manager_cash.py 58.58% <0.00%> (-0.91%) ⬇️
spp_programs/models/managers/payment_manager.py 44.30% <52.38%> (+14.89%) ⬆️

... and 44 files with indirect coverage changes

🚀 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.

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.

1 participant