diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml
index 33cf916..75a6963 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.05.00
+# VERSION: 01.00.00
# 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 c530e3e..67abe57 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.05.00
+ 01.05.04
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
index 6b20186..20c7cf3 100644
--- a/source/packages/plg_system_mokosuitenpo/src/Helper/DonorRetentionHelper.php
+++ b/source/packages/plg_system_mokosuitenpo/src/Helper/DonorRetentionHelper.php
@@ -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() ?: [];
diff --git a/source/packages/plg_system_mokosuitenpo/src/Helper/InKindDonationHelper.php b/source/packages/plg_system_mokosuitenpo/src/Helper/InKindDonationHelper.php
new file mode 100644
index 0000000..07bdc93
--- /dev/null
+++ b/source/packages/plg_system_mokosuitenpo/src/Helper/InKindDonationHelper.php
@@ -0,0 +1,96 @@
+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() ?: [];
+ }
+}
diff --git a/source/pkg_mokosuitenpo.xml b/source/pkg_mokosuitenpo.xml
index 44fa230..edc0f4d 100644
--- a/source/pkg_mokosuitenpo.xml
+++ b/source/pkg_mokosuitenpo.xml
@@ -2,7 +2,7 @@
Package - MokoSuite NPO
mokosuitenpo
- 01.05.00
+ 01.05.04
2026-06-11
Moko Consulting
hello@mokoconsulting.tech