diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 75a6963..8038885 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 01.00.00 +# VERSION: 01.04.02 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/source/packages/com_mokosuitenpo/mokosuitenpo.xml b/source/packages/com_mokosuitenpo/mokosuitenpo.xml index a9d716e..17839f0 100644 --- a/source/packages/com_mokosuitenpo/mokosuitenpo.xml +++ b/source/packages/com_mokosuitenpo/mokosuitenpo.xml @@ -7,7 +7,7 @@ GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 01.04.00 + 01.04.02 8.3 MokoSuite NPO component Moko\Component\MokoSuiteNpo diff --git a/source/packages/plg_system_mokosuitenpo/src/Helper/DonorRetentionHelper.php b/source/packages/plg_system_mokosuitenpo/src/Helper/DonorRetentionHelper.php new file mode 100644 index 0000000..6b20186 --- /dev/null +++ b/source/packages/plg_system_mokosuitenpo/src/Helper/DonorRetentionHelper.php @@ -0,0 +1,120 @@ +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; + } +} diff --git a/source/pkg_mokosuitenpo.xml b/source/pkg_mokosuitenpo.xml index 1d740f9..1928507 100644 --- a/source/pkg_mokosuitenpo.xml +++ b/source/pkg_mokosuitenpo.xml @@ -2,7 +2,7 @@ Package - MokoSuite NPO mokosuitenpo - 01.04.00 + 01.04.02 2026-06-11 Moko Consulting hello@mokoconsulting.tech