feat: InKindDonationHelper + DonorRetentionHelper fixes #22
@@ -5,7 +5,7 @@
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: mokocli.Automation
|
||||
# VERSION: 01.05.00
|
||||
# VERSION: 01.00.00
|
||||
# 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.05.00</version>
|
||||
<version>01.05.04</version>
|
||||
<php_minimum>8.3</php_minimum>
|
||||
<description>MokoSuite NPO component</description>
|
||||
<namespace path="src">Moko\Component\MokoSuiteNpo</namespace>
|
||||
|
||||
@@ -28,7 +28,7 @@ class DonorRetentionHelper
|
||||
->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')
|
||||
->group('cd.id, cd.name, cd.email_to, cd.telephone')
|
||||
->order('last_year_total DESC'));
|
||||
|
||||
return $db->loadObjectList() ?: [];
|
||||
@@ -53,7 +53,7 @@ class DonorRetentionHelper
|
||||
->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')
|
||||
->group('cd.id, cd.name, cd.email_to')
|
||||
->order('lifetime_total DESC'));
|
||||
|
||||
return $db->loadObjectList() ?: [];
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
namespace Moko\Plugin\System\MokoSuiteNpo\Helper;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\Database\DatabaseInterface;
|
||||
|
||||
/**
|
||||
* In-kind donation tracking — non-cash gifts, fair market valuation, category reporting.
|
||||
*/
|
||||
class InKindDonationHelper
|
||||
{
|
||||
/**
|
||||
* Record an in-kind donation.
|
||||
*/
|
||||
public static function record(int $contactId, string $description, float $fairMarketValue, string $category = 'goods'): object
|
||||
{
|
||||
if ($fairMarketValue <= 0) {
|
||||
throw new \InvalidArgumentException('Fair market value must be positive.');
|
||||
}
|
||||
|
||||
$allowedCategories = ['goods', 'services', 'equipment', 'real_estate', 'securities', 'vehicles', 'other'];
|
||||
if (!in_array($category, $allowedCategories, true)) {
|
||||
$category = 'other';
|
||||
}
|
||||
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
$filter = \Joomla\Filter\InputFilter::getInstance();
|
||||
|
||||
$donation = (object) [
|
||||
'contact_id' => $contactId,
|
||||
'description' => $filter->clean($description, 'STRING'),
|
||||
'fair_market_value'=> $fairMarketValue,
|
||||
'category' => $category,
|
||||
'status' => 'received',
|
||||
'received_at' => Factory::getDate()->toSql(),
|
||||
];
|
||||
|
||||
$db->insertObject('#__mokosuitenpo_inkind_donations', $donation, 'id');
|
||||
|
||||
return (object) ['success' => true, 'donation_id' => (int) $donation->id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get in-kind donation summary by category for a period.
|
||||
*/
|
||||
public static function getSummary(string $from = '', string $to = ''): array
|
||||
{
|
||||
$from = $from ?: date('Y-01-01');
|
||||
$to = $to ?: date('Y-m-d');
|
||||
|
||||
if (!\DateTime::createFromFormat('Y-m-d', $from) || !\DateTime::createFromFormat('Y-m-d', $to)) {
|
||||
throw new \InvalidArgumentException('Date parameters must be Y-m-d format.');
|
||||
}
|
||||
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('ik.category')
|
||||
->select('COUNT(*) AS donation_count')
|
||||
->select('SUM(ik.fair_market_value) AS total_value')
|
||||
->select('AVG(ik.fair_market_value) AS avg_value')
|
||||
->from($db->quoteName('#__mokosuitenpo_inkind_donations', 'ik'))
|
||||
->where('DATE(ik.received_at) BETWEEN ' . $db->quote($from) . ' AND ' . $db->quote($to))
|
||||
->group('ik.category')
|
||||
->order('total_value DESC'));
|
||||
|
||||
$results = $db->loadObjectList() ?: [];
|
||||
|
||||
foreach ($results as &$r) {
|
||||
$r->avg_value = round((float) $r->avg_value, 2);
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get donations needing appraisal (over $5,000 threshold per IRS rules).
|
||||
*/
|
||||
public static function getNeedingAppraisal(): array
|
||||
{
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('ik.*, cd.name AS donor_name')
|
||||
->from($db->quoteName('#__mokosuitenpo_inkind_donations', 'ik'))
|
||||
->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = ik.contact_id')
|
||||
->where('ik.fair_market_value > 5000')
|
||||
->where('ik.appraisal_date IS NULL')
|
||||
->where($db->quoteName('ik.category') . ' NOT IN (' . $db->quote('securities') . ')')
|
||||
->order('ik.fair_market_value DESC'));
|
||||
|
||||
return $db->loadObjectList() ?: [];
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
<extension type="package" method="upgrade">
|
||||
<name>Package - MokoSuite NPO</name>
|
||||
<packagename>mokosuitenpo</packagename>
|
||||
<version>01.05.00</version>
|
||||
<version>01.05.04</version>
|
||||
<creationDate>2026-06-11</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
Reference in New Issue
Block a user