|
|
|
@@ -0,0 +1,120 @@
|
|
|
|
|
<?php
|
|
|
|
|
namespace Moko\Plugin\System\MokoSuiteNpo\Helper;
|
|
|
|
|
|
|
|
|
|
defined('_JEXEC') or die;
|
|
|
|
|
|
|
|
|
|
use Joomla\CMS\Factory;
|
|
|
|
|
use Joomla\Database\DatabaseInterface;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Donor retention analysis — LYBUNT/SYBUNT detection, retention rates, lapsed outreach lists.
|
|
|
|
|
*/
|
|
|
|
|
class DonorRetentionHelper
|
|
|
|
|
{
|
|
|
|
|
/**
|
|
|
|
|
* Get LYBUNT donors — gave Last Year But Unfortunately Not This year.
|
|
|
|
|
*/
|
|
|
|
|
public static function getLybunt(int $currentYear = 0): array
|
|
|
|
|
{
|
|
|
|
|
$currentYear = $currentYear ?: (int) date('Y');
|
|
|
|
|
$lastYear = $currentYear - 1;
|
|
|
|
|
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
|
|
|
|
|
|
|
|
|
$db->setQuery($db->getQuery(true)
|
|
|
|
|
->select('cd.id AS contact_id, cd.name, cd.email_to, cd.telephone')
|
|
|
|
|
->select('MAX(d.donation_date) AS last_donation_date')
|
|
|
|
|
->select('SUM(CASE WHEN YEAR(d.donation_date) = ' . $lastYear . ' THEN d.amount ELSE 0 END) AS last_year_total')
|
|
|
|
|
->from($db->quoteName('#__contact_details', 'cd'))
|
|
|
|
|
->join('INNER', $db->quoteName('#__mokosuitenpo_donations', 'd') . ' ON d.contact_id = cd.id')
|
|
|
|
|
->where('YEAR(d.donation_date) = ' . $lastYear)
|
|
|
|
|
->where('cd.id NOT IN (SELECT d2.contact_id FROM #__mokosuitenpo_donations d2 WHERE YEAR(d2.donation_date) = ' . $currentYear . ')')
|
|
|
|
|
->group('cd.id')
|
|
|
|
|
->order('last_year_total DESC'));
|
|
|
|
|
|
|
|
|
|
return $db->loadObjectList() ?: [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get SYBUNT donors — gave Some Year But Unfortunately Not This year.
|
|
|
|
|
*/
|
|
|
|
|
public static function getSybunt(int $currentYear = 0, int $lookbackYears = 3): array
|
|
|
|
|
{
|
|
|
|
|
$currentYear = $currentYear ?: (int) date('Y');
|
|
|
|
|
$lastYear = $currentYear - 1;
|
|
|
|
|
$startYear = $currentYear - $lookbackYears;
|
|
|
|
|
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
|
|
|
|
|
|
|
|
|
$db->setQuery($db->getQuery(true)
|
|
|
|
|
->select('cd.id AS contact_id, cd.name, cd.email_to')
|
|
|
|
|
->select('COUNT(DISTINCT YEAR(d.donation_date)) AS years_donated')
|
|
|
|
|
->select('MAX(d.donation_date) AS last_donation_date')
|
|
|
|
|
->select('SUM(d.amount) AS lifetime_total')
|
|
|
|
|
->from($db->quoteName('#__contact_details', 'cd'))
|
|
|
|
|
->join('INNER', $db->quoteName('#__mokosuitenpo_donations', 'd') . ' ON d.contact_id = cd.id')
|
|
|
|
|
->where('YEAR(d.donation_date) BETWEEN ' . $startYear . ' AND ' . $lastYear)
|
|
|
|
|
->where('cd.id NOT IN (SELECT d2.contact_id FROM #__mokosuitenpo_donations d2 WHERE YEAR(d2.donation_date) = ' . $currentYear . ')')
|
|
|
|
|
->group('cd.id')
|
|
|
|
|
->order('lifetime_total DESC'));
|
|
|
|
|
|
|
|
|
|
return $db->loadObjectList() ?: [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Calculate retention rate — percentage of donors who gave again this year.
|
|
|
|
|
*/
|
|
|
|
|
public static function getRetentionRate(int $currentYear = 0): object
|
|
|
|
|
{
|
|
|
|
|
$currentYear = $currentYear ?: (int) date('Y');
|
|
|
|
|
$lastYear = $currentYear - 1;
|
|
|
|
|
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
|
|
|
|
|
|
|
|
|
$db->setQuery($db->getQuery(true)
|
|
|
|
|
->select('COUNT(DISTINCT d.contact_id) AS last_year_donors')
|
|
|
|
|
->from($db->quoteName('#__mokosuitenpo_donations', 'd'))
|
|
|
|
|
->where('YEAR(d.donation_date) = ' . $lastYear));
|
|
|
|
|
$lastYearDonors = (int) $db->loadResult();
|
|
|
|
|
|
|
|
|
|
$db->setQuery($db->getQuery(true)
|
|
|
|
|
->select('COUNT(DISTINCT d.contact_id) AS retained')
|
|
|
|
|
->from($db->quoteName('#__mokosuitenpo_donations', 'd'))
|
|
|
|
|
->where('YEAR(d.donation_date) = ' . $currentYear)
|
|
|
|
|
->where('d.contact_id IN (SELECT d2.contact_id FROM #__mokosuitenpo_donations d2 WHERE YEAR(d2.donation_date) = ' . $lastYear . ')'));
|
|
|
|
|
$retained = (int) $db->loadResult();
|
|
|
|
|
|
|
|
|
|
return (object) [
|
|
|
|
|
'last_year_donors' => $lastYearDonors,
|
|
|
|
|
'retained' => $retained,
|
|
|
|
|
'lapsed' => $lastYearDonors - $retained,
|
|
|
|
|
'retention_rate' => $lastYearDonors > 0 ? round($retained / $lastYearDonors * 100, 1) : 0,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get donor giving trends — year-over-year comparison.
|
|
|
|
|
*/
|
|
|
|
|
public static function getGivingTrends(int $years = 5): array
|
|
|
|
|
{
|
|
|
|
|
$currentYear = (int) date('Y');
|
|
|
|
|
$startYear = $currentYear - $years + 1;
|
|
|
|
|
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
|
|
|
|
|
|
|
|
|
$db->setQuery($db->getQuery(true)
|
|
|
|
|
->select('YEAR(donation_date) AS year')
|
|
|
|
|
->select('COUNT(*) AS donation_count')
|
|
|
|
|
->select('COUNT(DISTINCT contact_id) AS unique_donors')
|
|
|
|
|
->select('SUM(amount) AS total_amount')
|
|
|
|
|
->select('AVG(amount) AS avg_donation')
|
|
|
|
|
->from('#__mokosuitenpo_donations')
|
|
|
|
|
->where('YEAR(donation_date) BETWEEN ' . $startYear . ' AND ' . $currentYear)
|
|
|
|
|
->group('YEAR(donation_date)')
|
|
|
|
|
->order('year ASC'));
|
|
|
|
|
|
|
|
|
|
$trends = $db->loadObjectList() ?: [];
|
|
|
|
|
|
|
|
|
|
foreach ($trends as &$t) {
|
|
|
|
|
$t->avg_donation = round((float) $t->avg_donation, 2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $trends;
|
|
|
|
|
}
|
|
|
|
|
}
|