From 2b6d52eeb3ee6ed0e5a460bd385fb50e2bc1cfc3 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 13 Jun 2026 07:10:16 -0500 Subject: [PATCH] feat: webservices plugin, task scheduler, router, config, access NpoAutomation task plugin: year-end tax receipts, lapsed donor detection, grant deadline reminders, pledge follow-up emails. MokoSuiteNpoApi webservices: 6 CRUD routes (donors, donations, campaigns, grants, volunteers, events). Router, config.xml, access.xml, updates.xml. --- .../com_mokosuitenpo/admin/access.xml | 11 ++ .../com_mokosuitenpo/admin/config.xml | 13 ++ .../site/src/Service/Router.php | 21 +++ .../src/Extension/NpoAutomation.php | 133 ++++++++++++++++++ .../src/Extension/MokoSuiteNpoApi.php | 27 ++++ 5 files changed, 205 insertions(+) create mode 100644 source/packages/com_mokosuitenpo/admin/access.xml create mode 100644 source/packages/com_mokosuitenpo/admin/config.xml create mode 100644 source/packages/com_mokosuitenpo/site/src/Service/Router.php create mode 100644 source/packages/plg_task_mokosuitenpo/src/Extension/NpoAutomation.php create mode 100644 source/packages/plg_webservices_mokosuitenpo/src/Extension/MokoSuiteNpoApi.php diff --git a/source/packages/com_mokosuitenpo/admin/access.xml b/source/packages/com_mokosuitenpo/admin/access.xml new file mode 100644 index 0000000..352c7ca --- /dev/null +++ b/source/packages/com_mokosuitenpo/admin/access.xml @@ -0,0 +1,11 @@ + + +
+ + + + + + +
+
diff --git a/source/packages/com_mokosuitenpo/admin/config.xml b/source/packages/com_mokosuitenpo/admin/config.xml new file mode 100644 index 0000000..dc814a6 --- /dev/null +++ b/source/packages/com_mokosuitenpo/admin/config.xml @@ -0,0 +1,13 @@ + + +
+ + + +
+
+ + + +
+
diff --git a/source/packages/com_mokosuitenpo/site/src/Service/Router.php b/source/packages/com_mokosuitenpo/site/src/Service/Router.php new file mode 100644 index 0000000..e3c5688 --- /dev/null +++ b/source/packages/com_mokosuitenpo/site/src/Service/Router.php @@ -0,0 +1,21 @@ + [ + 'langConstPrefix' => 'PLG_TASK_MOKOSUITENPO_RECEIPTS_YEAREND', + 'method' => 'generateYearEndReceipts', + ], + 'mokosuite.npo.donors.lapsed' => [ + 'langConstPrefix' => 'PLG_TASK_MOKOSUITENPO_DONORS_LAPSED', + 'method' => 'detectLapsedDonors', + ], + 'mokosuite.npo.grants.reminders' => [ + 'langConstPrefix' => 'PLG_TASK_MOKOSUITENPO_GRANTS_REMINDERS', + 'method' => 'sendGrantReminders', + ], + 'mokosuite.npo.pledges.followup' => [ + 'langConstPrefix' => 'PLG_TASK_MOKOSUITENPO_PLEDGES_FOLLOWUP', + 'method' => 'followUpPledges', + ], + ]; + + public static function getSubscribedEvents(): array + { + return [ + 'onExecuteTask' => 'standardRoutineHandler', + 'onContentPrepareForm' => 'enhanceTaskItemForm', + 'onTaskOptionsList' => 'advertiseRoutines', + ]; + } + + private function generateYearEndReceipts(ExecuteTaskEvent $event): int + { + $count = \Moko\Plugin\System\MokoSuiteNpo\Helper\TaxReceiptHelper::generateYearEndReceipts((int) date('Y') - 1); + Log::add("NPO year-end receipts: generated {$count}", Log::INFO, 'mokosuite.npo'); + return Status::OK; + } + + private function detectLapsedDonors(ExecuteTaskEvent $event): int + { + $db = Factory::getContainer()->get(DatabaseInterface::class); + + $db->setQuery($db->getQuery(true) + ->update('#__mokosuitenpo_donors') + ->set($db->quoteName('donor_level') . ' = ' . $db->quote('lapsed')) + ->where($db->quoteName('last_gift_date') . ' < DATE_SUB(NOW(), INTERVAL 12 MONTH)') + ->where($db->quoteName('gift_count') . ' > 0') + ->where($db->quoteName('donor_level') . ' != ' . $db->quote('lapsed'))); + $db->execute(); + + $lapsed = $db->getAffectedRows(); + if ($lapsed > 0) { + Log::add("NPO lapsed donors: {$lapsed} marked as lapsed", Log::INFO, 'mokosuite.npo'); + } + return Status::OK; + } + + private function sendGrantReminders(ExecuteTaskEvent $event): int + { + $db = Factory::getContainer()->get(DatabaseInterface::class); + + $db->setQuery($db->getQuery(true) + ->select('*') + ->from('#__mokosuitenpo_grants') + ->where($db->quoteName('status') . ' IN (' . $db->quote('prospect') . ',' . $db->quote('writing') . ')') + ->where($db->quoteName('application_deadline') . ' BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 14 DAY)')); + $grants = $db->loadObjectList() ?: []; + + if (!empty($grants)) { + $body = "Grant deadlines approaching:\n\n"; + foreach ($grants as $g) { + $days = max(0, round((strtotime($g->application_deadline) - time()) / 86400)); + $body .= "- {$g->title} ({$g->funder_name}) — {$days} days left\n"; + } + + $mailer = Factory::getMailer(); + $mailer->addRecipient(Factory::getApplication()->get('mailfrom')); + $mailer->setSubject('NPO: ' . count($grants) . ' grant deadlines approaching'); + $mailer->setBody($body); + $mailer->Send(); + } + + return Status::OK; + } + + private function followUpPledges(ExecuteTaskEvent $event): int + { + $db = Factory::getContainer()->get(DatabaseInterface::class); + + $db->setQuery($db->getQuery(true) + ->select('p.*, cd.name AS donor_name, cd.email_to') + ->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($db->quoteName('p.due_date') . ' BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 14 DAY)')); + $pledges = $db->loadObjectList() ?: []; + + $sent = 0; + foreach ($pledges as $p) { + if (!$p->email_to) continue; + $remaining = (float) $p->total_amount - (float) $p->amount_fulfilled; + + $mailer = Factory::getMailer(); + $mailer->addRecipient($p->email_to, $p->donor_name); + $mailer->setSubject('Pledge reminder — $' . number_format($remaining, 2) . ' remaining'); + $mailer->setBody("Hi {$p->donor_name},\n\nThis is a friendly reminder about your pledge of \${$p->total_amount}. Your remaining balance is \${$remaining}.\n\nThank you for your generosity!"); + $mailer->Send(); + $sent++; + } + + Log::add("NPO pledge follow-up: sent {$sent} reminders", Log::INFO, 'mokosuite.npo'); + return Status::OK; + } +} diff --git a/source/packages/plg_webservices_mokosuitenpo/src/Extension/MokoSuiteNpoApi.php b/source/packages/plg_webservices_mokosuitenpo/src/Extension/MokoSuiteNpoApi.php new file mode 100644 index 0000000..d7d0764 --- /dev/null +++ b/source/packages/plg_webservices_mokosuitenpo/src/Extension/MokoSuiteNpoApi.php @@ -0,0 +1,27 @@ + 'onBeforeApiRoute']; + } + + public function onBeforeApiRoute(BeforeApiRouteEvent $event): void + { + $router = $event->getRouter(); + $router->createCRUDRoutes('v1/mokosuite/npo/donors', 'npodonors', ['component' => 'com_mokosuitenpo']); + $router->createCRUDRoutes('v1/mokosuite/npo/donations', 'npodonations', ['component' => 'com_mokosuitenpo']); + $router->createCRUDRoutes('v1/mokosuite/npo/campaigns', 'npocampaigns', ['component' => 'com_mokosuitenpo']); + $router->createCRUDRoutes('v1/mokosuite/npo/grants', 'npogrants', ['component' => 'com_mokosuitenpo']); + $router->createCRUDRoutes('v1/mokosuite/npo/volunteers', 'npovolunteers', ['component' => 'com_mokosuitenpo']); + $router->createCRUDRoutes('v1/mokosuite/npo/events', 'npoevents', ['component' => 'com_mokosuitenpo']); + } +}