-
Notifications
You must be signed in to change notification settings - Fork 0
[WIP] CIVIMM-462: Add instalment generation scheduled job for recurring contributions #12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
Summary of ChangesHello @erawat, 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 introduces a significant automation feature to CiviCRM by implementing a scheduled job that generates instalment contributions for recurring donations. This job streamlines the process of creating "invoice" records for non-membership recurring contributions that are due, ensuring timely record creation without manual intervention or reliance on processor-specific logic. It focuses solely on record generation, explicitly avoiding any payment processor communication, and includes robust checks for idempotency and membership exclusion. Highlights
Using Gemini Code AssistThe 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
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 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. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this 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 a scheduled job to generate instalments for recurring contributions, which is a valuable addition. The implementation is well-structured, featuring a dedicated service, an API endpoint, and a managed job definition. The inclusion of comprehensive unit tests is particularly commendable.
I have identified two high-severity issues. The first is a logic bug in how due contributions are queried, which could lead to instalments being processed a day late. The second concerns overly defensive code that can mask data problems and lead to incorrect behavior when checking for existing instalments. I've provided specific suggestions to address these points. With these changes, this will be a very solid feature.
| if (!is_array($recur)) { | ||
| continue; | ||
| } | ||
| $recurId = is_numeric($recur['id']) ? (int) $recur['id'] : 0; | ||
| $receiveDate = is_string($recur['next_sched_contribution_date']) ? $recur['next_sched_contribution_date'] : ''; | ||
| $frequencyUnit = is_string($recur['frequency_unit:name']) ? $recur['frequency_unit:name'] : 'month'; | ||
| $frequencyInterval = is_numeric($recur['frequency_interval']) ? (int) $recur['frequency_interval'] : 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The variable extraction in this block is overly defensive and can mask underlying data issues. Specifically, if next_sched_contribution_date is null, $receiveDate defaults to an empty string. This causes instalmentExists() to check for contributions on 1970-01-01 because strtotime('') evaluates to false. This is incorrect and could lead to unexpected behavior, such as skipping the creation of a valid instalment.
It's safer to fail fast if critical data is missing. The existing try/catch block will handle these exceptions gracefully by logging the error and moving to the next record. This change makes the code more robust and easier to debug.
if (!is_array($recur) || empty($recur['id']) || empty($recur['next_sched_contribution_date'])) {
throw new \InvalidArgumentException('Malformed recurring contribution record from query.');
}
$recurId = (int) $recur['id'];
$receiveDate = $recur['next_sched_contribution_date'];
$frequencyUnit = $recur['frequency_unit:name'] ?? 'month';
$frequencyInterval = (int) ($recur['frequency_interval'] ?? 1);There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch — the empty-string fallback for next_sched_contribution_date would silently check against 1970-01-01 via strtotime(''). Applied the fail-fast approach as suggested. The outer try/catch handles it gracefully.
| ['id', '=', 'm.contribution_recur_id'] | ||
| ) | ||
| ->addWhere('contribution_status_id:name', '=', 'In Progress') | ||
| ->addWhere('next_sched_contribution_date', '<=', date('Y-m-d')) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The query for due contributions uses date('Y-m-d'), which effectively compares next_sched_contribution_date against the start of the day (e.g., YYYY-MM-DD 00:00:00). Since next_sched_contribution_date is a DATETIME field, any contributions scheduled for later in the current day will be excluded by this query and only processed on the following day. This could cause a delay in generating instalments.
To ensure all due contributions are processed in a timely manner, it's better to compare against the current time. APIv4 supports relative date formats, so using 'now' is a clean and effective way to achieve this.
->addWhere('next_sched_contribution_date', '<=', 'now')There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This has already been addressed — the query now uses < nextDay 00:00:00 instead of <= date('Y-m-d'), which includes the entire current day.
1f6f405 to
52922aa
Compare
04461d8 to
e4000ea
Compare
e4000ea to
4fdc210
Compare
Overview
Adds a scheduled job that creates Pending contribution records for each due In Progress recurring contribution that is not linked to a membership. This is the "invoice generation" step — no payment processor calls are made.
Also introduces PHPStan stub files infrastructure for CiviCRM's dynamically-generated Api4 entity classes, reducing the baseline by 29 entries (393 → 364).
Before
Civi\Api4\Contribution, etc.) because these classes are generated at runtime.After
A new scheduled job (
InstalmentGenerator.Run) runs periodically and:next_sched_contribution_dateis due (using< nextDay 00:00:00precision)DEFAULT_PROCESSOR_TYPEconstant)Contribution.createusing a template-based approach (copies key fields from most recent Completed contribution)CRM_Core_Transactionfor atomicitynext_sched_contribution_datebyfrequency_unit × frequency_intervalreferenceDateparameter for testabilityPHPStan infrastructure improvements:
stubs/CiviApi4.stub.phpwith stub declarations for 7 Api4 entity classesstubFilesinphpstan.neonand CI workflowpsr/logas dev dependency (matching CiviCRM's^1.1constraint)PaymentProcessorCustomerBAO return type annotationsCLAUDE.mdwith PHPStan stub and baseline management instructionsTechnical Details
New files:
Civi/Paymentprocessingcore/Service/InstalmentGenerationService.php— Core service withDEFAULT_PROCESSOR_TYPEconstant and methods:generateInstalments(),getDueRecurringContributions(),instalmentExists(),createInstalment(),advanceScheduleDate()api/v3/InstalmentGenerator/Run.php— API3 endpoint withprocessor_typeandbatch_sizeparametersmanaged/Job_InstalmentGenerator.mgd.php— Managed scheduled job definitiontests/phpunit/.../InstalmentGenerationServiceTest.php— Tests covering all public methodsstubs/CiviApi4.stub.php— PHPStan stubs for dynamically-generated Api4 entity classesModified files:
Civi/Paymentprocessingcore/Hook/Container/ServiceContainer.php— Registerspaymentprocessingcore.instalment_generationserviceCRM/Paymentprocessingcore/BAO/PaymentProcessorCustomer.php— Fixed return type annotations with@varfor PHPStanphpstan.neon— AddedstubFilesandreportUnmatchedIgnoredErrors: false.github/workflows/phpstan.yml— AddedstubFilesandreportUnmatchedIgnoredErrorsto CI configphpstan-baseline.neon— Regenerated (393 → 364 entries)composer.json/composer.lock— Addedpsr/log ^1.1to require-devCLAUDE.md— Added PHPStan stub files and baseline management documentationDesign decisions:
Contribution.createwith template-based field copying instead ofrepeattransactionAPI3 — avoids side effects (receipt emails, unpredictable hook triggers, tax bugs)CRM_Core_Transactionwraps instalment creation + schedule advance for atomicity< nextDay 00:00:00instead of<= 23:59:59to avoid edge casesextends AbstractEntity— CI and local resolve different classes from scan directories, and extending causes "unknown parent class" errors in CIreportUnmatchedIgnoredErrors: false— necessary because local/CI scan different vendor paths, so some baseline patterns only match in one environmentCore overrides
None.
Comments
processor_type=Stripeandbatch_size=500but both are configurable via the scheduled job parametersCLAUDE.mdfor future Api4 entity additions