feat: PledgeReminderHelper + ThankYou view + incremental #20
@@ -5,7 +5,7 @@
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: mokocli.Automation
|
||||
# VERSION: 01.00.00
|
||||
# VERSION: 01.03.08
|
||||
# BRIEF: Auto-create feature branch when an issue is opened
|
||||
|
||||
name: "Universal: Issue Branch"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>01.03.00</version>
|
||||
<version>01.03.08</version>
|
||||
<php_minimum>8.3</php_minimum>
|
||||
<description>MokoSuite NPO component</description>
|
||||
<namespace path="src">Moko\Component\MokoSuiteNpo</namespace>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
namespace Moko\Component\MokoSuiteNpo\Site\View\ThankYou;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
use Joomla\Database\DatabaseInterface;
|
||||
|
||||
/**
|
||||
* Donation thank-you page — displays after successful donation with receipt info.
|
||||
*/
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
public ?object $donation = null;
|
||||
public ?object $receipt = null;
|
||||
|
||||
public function display($tpl = null): void
|
||||
{
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
$donationId = Factory::getApplication()->getInput()->getInt('id', 0);
|
||||
$token = Factory::getApplication()->getInput()->getString('token', '');
|
||||
|
||||
if (!$donationId || !$token) {
|
||||
parent::display($tpl);
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify token matches donation (prevents enumeration)
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('d.*, cd.name AS donor_name, f.name AS fund_name, c.title AS campaign_title')
|
||||
->from($db->quoteName('#__mokosuitenpo_donations', 'd'))
|
||||
->join('INNER', $db->quoteName('#__mokosuitenpo_donors', 'don') . ' ON don.id = d.donor_id')
|
||||
->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = don.contact_id')
|
||||
->join('LEFT', $db->quoteName('#__mokosuitenpo_funds', 'f') . ' ON f.id = d.fund_id')
|
||||
->join('LEFT', $db->quoteName('#__mokosuitenpo_campaigns', 'c') . ' ON c.id = d.campaign_id')
|
||||
->where('d.id = ' . (int) $donationId)
|
||||
->where($db->quoteName('d.confirmation_token') . ' = ' . $db->quote($token)));
|
||||
$this->donation = $db->loadObject();
|
||||
|
||||
if ($this->donation) {
|
||||
// Get tax receipt if generated
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('receipt_number, issued_date, amount')
|
||||
->from('#__mokosuitenpo_tax_receipts')
|
||||
->where('donation_id = ' . (int) $donationId));
|
||||
$this->receipt = $db->loadObject();
|
||||
}
|
||||
|
||||
parent::display($tpl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
namespace Moko\Plugin\System\MokoSuiteNpo\Helper;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\Database\DatabaseInterface;
|
||||
|
||||
/**
|
||||
* Grant reporting — deliverable tracking, spending reports, funder compliance.
|
||||
*/
|
||||
class GrantReportingHelper
|
||||
{
|
||||
/**
|
||||
* Get grant spending report — budgeted vs actual by category.
|
||||
*/
|
||||
public static function getSpendingReport(int $grantId): object
|
||||
{
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('g.*, cd.name AS funder_name')
|
||||
->from($db->quoteName('#__mokosuitenpo_grants', 'g'))
|
||||
->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = g.funder_contact_id')
|
||||
->where('g.id = ' . (int) $grantId));
|
||||
$grant = $db->loadObject();
|
||||
|
||||
if (!$grant) return (object) ['found' => false];
|
||||
|
||||
// Get expenses charged to this grant's fund
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('category, COUNT(*) AS count, COALESCE(SUM(amount), 0) AS spent')
|
||||
->from('#__mokosuitenpo_fund_expenses')
|
||||
->where('fund_id = ' . (int) ($grant->fund_id ?? 0))
|
||||
->group('category')
|
||||
->order('spent DESC'));
|
||||
$grant->spending_by_category = $db->loadObjectList() ?: [];
|
||||
|
||||
$grant->total_spent = array_sum(array_column($grant->spending_by_category, 'spent'));
|
||||
$grant->remaining = max(0, (float) ($grant->amount ?? 0) - (float) $grant->total_spent);
|
||||
$grant->utilization_pct = (float) ($grant->amount ?? 0) > 0
|
||||
? round((float) $grant->total_spent / (float) $grant->amount * 100, 1) : 0;
|
||||
|
||||
return $grant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get grants requiring reports soon.
|
||||
*/
|
||||
public static function getUpcomingReportDeadlines(int $days = 30): array
|
||||
{
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('g.id, g.title, g.funder, g.report_due_date, g.amount')
|
||||
->select('DATEDIFF(g.report_due_date, CURDATE()) AS days_until_due')
|
||||
->from($db->quoteName('#__mokosuitenpo_grants', 'g'))
|
||||
->where($db->quoteName('g.status') . ' IN (' . $db->quote('active') . ',' . $db->quote('reporting') . ')')
|
||||
->where($db->quoteName('g.report_due_date') . ' IS NOT NULL')
|
||||
->where($db->quoteName('g.report_due_date') . ' BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL ' . (int) $days . ' DAY)')
|
||||
->order('g.report_due_date ASC'));
|
||||
|
||||
return $db->loadObjectList() ?: [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
namespace Moko\Plugin\System\MokoSuiteNpo\Helper;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\Database\DatabaseInterface;
|
||||
|
||||
/**
|
||||
* Pledge reminders — notify donors of unfulfilled pledges, track fulfillment progress.
|
||||
*/
|
||||
class PledgeReminderHelper
|
||||
{
|
||||
/**
|
||||
* Get unfulfilled pledges that need reminders.
|
||||
*/
|
||||
public static function getUnfulfilled(int $overdueDays = 30): array
|
||||
{
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('p.*, cd.name AS donor_name, cd.email_to')
|
||||
->select('COALESCE((SELECT SUM(d.amount) FROM #__mokosuitenpo_donations d WHERE d.pledge_id = p.id), 0) AS amount_fulfilled')
|
||||
->from($db->quoteName('#__mokosuitenpo_pledges', 'p'))
|
||||
->join('INNER', $db->quoteName('#__mokosuitenpo_donors', 'don') . ' ON don.id = p.donor_id')
|
||||
->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = don.contact_id')
|
||||
->where($db->quoteName('p.status') . ' = ' . $db->quote('active'))
|
||||
->where('p.due_date IS NOT NULL')
|
||||
->where('p.due_date < DATE_SUB(CURDATE(), INTERVAL ' . (int) $overdueDays . ' DAY)')
|
||||
->having('amount_fulfilled < p.amount')
|
||||
->order('p.due_date ASC'));
|
||||
|
||||
$pledges = $db->loadObjectList() ?: [];
|
||||
|
||||
foreach ($pledges as &$p) {
|
||||
$p->remaining = round((float) $p->amount - (float) $p->amount_fulfilled, 2);
|
||||
$p->fulfillment_pct = (float) $p->amount > 0 ? round((float) $p->amount_fulfilled / (float) $p->amount * 100, 1) : 0;
|
||||
}
|
||||
|
||||
return $pledges;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pledge fulfillment summary.
|
||||
*/
|
||||
public static function getFulfillmentSummary(): object
|
||||
{
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('COUNT(*) AS total_pledges')
|
||||
->select('COALESCE(SUM(amount), 0) AS total_pledged')
|
||||
->select('COALESCE(SUM((SELECT COALESCE(SUM(d.amount), 0) FROM #__mokosuitenpo_donations d WHERE d.pledge_id = p.id)), 0) AS total_received')
|
||||
->from($db->quoteName('#__mokosuitenpo_pledges', 'p'))
|
||||
->where($db->quoteName('p.status') . ' IN (' . $db->quote('active') . ',' . $db->quote('completed') . ')'));
|
||||
|
||||
$stats = $db->loadObject() ?: (object) ['total_pledges' => 0, 'total_pledged' => 0, 'total_received' => 0];
|
||||
$stats->outstanding = round((float) $stats->total_pledged - (float) $stats->total_received, 2);
|
||||
$stats->fulfillment_rate = (float) $stats->total_pledged > 0
|
||||
? round((float) $stats->total_received / (float) $stats->total_pledged * 100, 1) : 0;
|
||||
|
||||
return $stats;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
<extension type="package" method="upgrade">
|
||||
<name>Package - MokoSuite NPO</name>
|
||||
<packagename>mokosuitenpo</packagename>
|
||||
<version>01.03.00</version>
|
||||
<version>01.03.08</version>
|
||||
<creationDate>2026-06-11</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
Reference in New Issue
Block a user