Prevent deposit PDF 500s, preserve AdvancedDeposit filters, and improve Financial Reports UX#7643
Prevent deposit PDF 500s, preserve AdvancedDeposit filters, and improve Financial Reports UX#7643
Conversation
… show user-friendly notification
…ayments (avoid inline alert page)
…s; HEAD returns 404 status
…ort; restore datetype logic The filterForAdvancedDeposit method was added without proper $datetype support. When I added $datetype handling, it used useDepositQuery() on a LEFT JOIN, which doesn't filter correctly. The original 5.22.0 SQL used INNER JOIN deposit_dep. Now: - When datetype='Deposit': use innerJoinWithDeposit() + useDepositQuery() for deposit date filtering - When datetype='Payment': use leftJoinWithDeposit() for optional deposits, filter by plg_date - This matches the original SQL behavior and fixes reports showing no results when filtering by deposit date
… no-data redirect; use FinancialService for ORM data
… apply date filtering accordingly
…or all financial reports AdvancedDeposit, TaxReport, and ZeroGivers now consistently preserve date filters and report-specific parameters (datetype for AdvancedDeposit) when redirecting after no data is returned for CSV export. This matches the PDF no-data redirect behavior.
There was a problem hiding this comment.
Pull request overview
This PR addresses critical bugs in deposit PDF generation and Advanced Deposit Report filtering. The main changes restore datetype filtering (Deposit vs Payment date) for the Advanced Deposit Report, add client-side and server-side guards to prevent 500 errors when generating PDFs for deposits without payments, preserve filter parameters across CSV export redirects, and improve Financial Reports UI by extracting inline styles to SCSS.
Key Changes:
- Adds datetype parameter to
PledgeQuery::filterForAdvancedDeposit()to support filtering by either payment date or deposit date - Implements client-side and API-level validation to prevent PDF generation for empty deposits
- Preserves date range and datetype parameters when redirecting back from "No Data" CSV exports
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/ChurchCRM/model/ChurchCRM/PledgeQuery.php | Adds datetype parameter and conditional INNER/LEFT JOIN logic for deposit date filtering; modifies payment method filtering approach |
| src/ChurchCRM/Service/FinancialService.php | Passes datetype parameter through to PledgeQuery |
| src/api/routes/finance/finance-deposits.php | Adds payment count validation before PDF generation with 404 response for empty deposits |
| src/Reports/PrintDeposit.php | Adds server-side guard that redirects with warning message when deposit has no payments |
| src/skin/js/FindDepositSlip.js | Implements client-side AJAX check for payments before allowing PDF export |
| src/skin/js/DepositSlipEditor.js | Implements client-side AJAX check for payments before PDF generation |
| src/Reports/AdvancedDeposit.php | Preserves DateStart, DateEnd, and datetype parameters in redirect URL |
| src/FinancialReports.php | Extracts and preserves date/datetype parameters from GET/POST for form repopulation |
| src/skin/scss/_financial-reports.scss | New SCSS file containing styles extracted from inline PHP |
| src/skin/churchcrm.scss | Imports the new _financial-reports.scss module |
| * @param array $familyIds Optional family IDs to filter | ||
| * @param array $methods Optional payment methods to filter | ||
| * @param array $classificationIds Optional classification IDs to filter | ||
| * @param string $datetype 'Payment' for pledge date, 'Deposit' for deposit date |
There was a problem hiding this comment.
The PHPDoc comment states "'Payment' for pledge date" but the parameter name is $datetype and the valid values are 'Payment' or 'Deposit'. This is confusing.
The comment should clarify what 'Payment' means in this context - it filters by the payment date (plg_date), not the deposit date.
Recommendation: Update the comment to be clearer:
* @param string $datetype 'Payment' for payment date (plg_date), 'Deposit' for deposit date (dep_date)| * @param string $datetype 'Payment' for pledge date, 'Deposit' for deposit date | |
| * @param string $datetype 'Payment' for payment date (plg_date), 'Deposit' for deposit date (dep_date) |
| foreach ($methods as $method) { | ||
| $this->addOr(PledgeQuery::create()->filterByMethod($method)); | ||
| } |
There was a problem hiding this comment.
The payment method filtering logic is incorrect. Using addOr() with a new query instance doesn't combine filters properly in Propel. This will likely produce incorrect SQL.
The original code $this->filterByMethod($methods, \Propel\Runtime\ActiveQuery\Criteria::IN) was correct for filtering by multiple payment methods using an IN clause. The new loop-based approach with addOr() won't work as intended.
Recommendation: Revert this change to use the original IN clause approach:
if (!empty($methods)) {
$this->filterByMethod($methods, \Propel\Runtime\ActiveQuery\Criteria::IN);
}| // For payment date, left join is fine since deposit is optional | ||
| $this->leftJoinWithDeposit(); | ||
| } | ||
|
|
There was a problem hiding this comment.
Trailing whitespace on line 113. This line contains only spaces after the closing brace, which should be removed for code cleanliness.
src/skin/js/FindDepositSlip.js
Outdated
| $.each(selectedRows, function (index, value) { | ||
| window.CRM.VerifyThenLoadAPIContent( | ||
| window.CRM.root + "/api/deposits/" + value.Id + "/" + type, | ||
| ); | ||
| var url = window.CRM.root + "/api/deposits/" + value.Id + "/" + type; | ||
| if (type === 'pdf') { | ||
| // Check payments first; if none, notify and skip | ||
| $.ajax({ | ||
| method: 'GET', | ||
| url: window.CRM.root + '/api/deposits/' + value.Id + '/payments', | ||
| dataType: 'json' | ||
| }).done(function(data) { | ||
| var count = Array.isArray(data) ? data.length : 0; | ||
| if (count === 0) { | ||
| window.CRM.notify(i18next.t('No payments on this deposit'), { type: 'warning', delay: 5000 }); | ||
| return; | ||
| } | ||
| window.CRM.VerifyThenLoadAPIContent(url); | ||
| }).fail(function(jqXHR) { | ||
| var errorMsg = i18next.t('There was a problem retrieving the requested object'); | ||
| if (jqXHR.responseJSON && jqXHR.responseJSON.message) { errorMsg = jqXHR.responseJSON.message; } | ||
| window.CRM.notify(errorMsg, { type: 'danger', delay: 7000 }); | ||
| }); | ||
| } else { | ||
| // csv or other types - proceed as before | ||
| window.CRM.VerifyThenLoadAPIContent(url); | ||
| } | ||
| }); |
There was a problem hiding this comment.
When multiple deposits are selected for PDF export, the AJAX requests are fired asynchronously in parallel (via $.each). If one fails, the others continue processing, which could lead to race conditions and inconsistent user feedback.
Additionally, if a user selects 10 deposits with no payments, they'll receive 10 separate warning notifications that may overlap or confuse the user.
Recommendation: Consider batching the validation check before the loop, or showing a consolidated message about which deposits failed validation.
| return SlimUtils::renderJson($response->withStatus(404), ['message' => 'No Payments on this Deposit']); | ||
| } | ||
|
|
||
| $deposit = DepositQuery::create()->findOneById($id); |
There was a problem hiding this comment.
Missing null check for the deposit. If a deposit with the given ID doesn't exist, findOneById() returns null, and calling getPDF() on null will cause a fatal error.
Recommendation: Add a null check before calling getPDF():
$deposit = DepositQuery::create()->findOneById($id);
if ($deposit === null) {
return SlimUtils::renderJson($response->withStatus(404), ['message' => 'Deposit not found']);
}
$deposit->getPDF();| $deposit = DepositQuery::create()->findOneById($id); | |
| $deposit = DepositQuery::create()->findOneById($id); | |
| if ($deposit === null) { | |
| return SlimUtils::renderJson($response->withStatus(404), ['message' => 'Deposit not found']); | |
| } |
src/Reports/PrintDeposit.php
Outdated
| RedirectUtils::absoluteRedirect($_SERVER['HTTP_REFERER']); | ||
| } else { | ||
| RedirectUtils::redirect('DepositSlipEditor.php?DepositSlipID=' . (int)$iDepositSlipID); | ||
| } |
There was a problem hiding this comment.
Potential open redirect vulnerability. Using $_SERVER['HTTP_REFERER'] directly in RedirectUtils::absoluteRedirect() without validation allows an attacker to control the redirect destination by manipulating the Referer header.
An attacker could craft a malicious link that redirects users to an external phishing site after they attempt to generate a PDF for an empty deposit.
Recommendation: Validate that the referer URL belongs to the same domain before redirecting:
if (array_key_exists('HTTP_REFERER', $_SERVER) && !empty($_SERVER['HTTP_REFERER'])) {
$referer = $_SERVER['HTTP_REFERER'];
$parsedReferer = parse_url($referer);
$parsedRoot = parse_url(SystemURLs::getRootPath());
if (isset($parsedReferer['host']) && $parsedReferer['host'] === $parsedRoot['host']) {
RedirectUtils::absoluteRedirect($referer);
exit;
}
}
RedirectUtils::redirect('DepositSlipEditor.php?DepositSlipID=' . (int)$iDepositSlipID);| RedirectUtils::absoluteRedirect($_SERVER['HTTP_REFERER']); | |
| } else { | |
| RedirectUtils::redirect('DepositSlipEditor.php?DepositSlipID=' . (int)$iDepositSlipID); | |
| } | |
| $referer = $_SERVER['HTTP_REFERER']; | |
| $parsedReferer = parse_url($referer); | |
| $parsedRoot = parse_url(SystemURLs::getRootPath()); | |
| if (isset($parsedReferer['host']) && $parsedReferer['host'] === $parsedRoot['host']) { | |
| RedirectUtils::absoluteRedirect($referer); | |
| exit; | |
| } | |
| } | |
| RedirectUtils::redirect('DepositSlipEditor.php?DepositSlipID=' . (int)$iDepositSlipID); |
- Clarify PledgeQuery datetype PHPDoc (payment vs deposit date filtering) - Remove trailing whitespace in PledgeQuery - Improve batch PDF export validation in FindDepositSlip (consolidate messages) * Validate all deposits before starting downloads * Show single summary message instead of 10 separate notifications * Prevents UI confusion and race conditions
What Changed
Critical Bug (PledgeQuery): filterForAdvancedDeposit() was using LEFT JOIN for deposit date filtering, which doesn't enforce the deposit must exist. Fixed by using innerJoinWithDeposit() when datetype='Deposit' fix #7638
AdvancedDeposit CSV Export: No-data redirect wasn't preserving datetype + date parameters. Fixed.
TaxReport CSV Export: No-data redirect wasn't preserving date parameters. Fixed.
ZeroGivers CSV Export: No-data redirect wasn't preserving date parameters. Fixed.
Restores Advanced Deposit Report datetype filtering (Deposit vs Payment date) and preserves date filters across redirects.
Fixes deposit PDF/CSV server errors and noisy logs when deposits have no payments.
Adds client-side pre-checks and user notifications to prevent invalid export attempts.
Improves Financial Reports UI: moves inline styles to SCSS, de-cramps filters, and fixes datepicker z-index.
Type
Testing
Screenshots
Security Check
Code Quality
Pre-Merge