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.
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<access component="com_mokosuitenpo">
|
||||
<section name="component">
|
||||
<action name="core.admin" title="JACTION_ADMIN" />
|
||||
<action name="core.manage" title="JACTION_MANAGE" />
|
||||
<action name="core.create" title="JACTION_CREATE" />
|
||||
<action name="core.edit" title="JACTION_EDIT" />
|
||||
<action name="npo.donations" title="Manage Donations" />
|
||||
<action name="npo.grants" title="Manage Grants" />
|
||||
</section>
|
||||
</access>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<config>
|
||||
<fieldset name="basic" label="NPO Settings">
|
||||
<field name="org_name" type="text" default="" label="Organization Name" />
|
||||
<field name="org_ein" type="text" default="" label="EIN / Tax ID" />
|
||||
<field name="fiscal_year_start" type="text" default="01-01" label="Fiscal Year Start (MM-DD)" />
|
||||
</fieldset>
|
||||
<fieldset name="donations" label="Donations">
|
||||
<field name="auto_receipt" type="radio" default="1" label="Auto Tax Receipts" class="btn-group btn-group-yesno"><option value="1">JYES</option><option value="0">JNO</option></field>
|
||||
<field name="receipt_prefix" type="text" default="RCP" label="Receipt Prefix" />
|
||||
<field name="min_receipt_amount" type="number" default="250" label="Min Receipt Amount ($)" />
|
||||
</fieldset>
|
||||
</config>
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
namespace Moko\Component\MokoSuiteNpo\Site\Service;
|
||||
defined('_JEXEC') or die;
|
||||
use Joomla\CMS\Component\Router\RouterBase;
|
||||
class Router extends RouterBase
|
||||
{
|
||||
public function build(&$query): array
|
||||
{
|
||||
$segments = [];
|
||||
if (isset($query['view'])) { $segments[] = $query['view']; unset($query['view']); }
|
||||
if (isset($query['id'])) { $segments[] = $query['id']; unset($query['id']); }
|
||||
return $segments;
|
||||
}
|
||||
public function parse(&$segments): array
|
||||
{
|
||||
$vars = [];
|
||||
if (!empty($segments[0])) $vars['view'] = array_shift($segments);
|
||||
if (!empty($segments[0]) && is_numeric($segments[0])) $vars['id'] = (int) array_shift($segments);
|
||||
return $vars;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
namespace Moko\Plugin\Task\MokoSuiteNpo\Extension;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Log\Log;
|
||||
use Joomla\CMS\Plugin\CMSPlugin;
|
||||
use Joomla\Component\Scheduler\Administrator\Event\ExecuteTaskEvent;
|
||||
use Joomla\Component\Scheduler\Administrator\Task\Status;
|
||||
use Joomla\Component\Scheduler\Administrator\Traits\TaskPluginTrait;
|
||||
use Joomla\Database\DatabaseInterface;
|
||||
use Joomla\Event\SubscriberInterface;
|
||||
|
||||
/**
|
||||
* NPO scheduled tasks: tax receipts, donor lapse detection, grant reminders, pledge follow-up.
|
||||
*/
|
||||
class NpoAutomation extends CMSPlugin implements SubscriberInterface
|
||||
{
|
||||
use TaskPluginTrait;
|
||||
|
||||
protected const TASKS_MAP = [
|
||||
'mokosuite.npo.receipts.yearend' => [
|
||||
'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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
namespace Moko\Plugin\WebServices\MokoSuiteNpo\Extension;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Plugin\CMSPlugin;
|
||||
use Joomla\CMS\Event\Application\BeforeApiRouteEvent;
|
||||
use Joomla\Event\SubscriberInterface;
|
||||
|
||||
final class MokoSuiteNpoApi extends CMSPlugin implements SubscriberInterface
|
||||
{
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return ['onBeforeApiRoute' => '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']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user