diff --git a/source/packages/com_mokosuitenpo/api/src/Controller/NpoReportsController.php b/source/packages/com_mokosuitenpo/api/src/Controller/NpoReportsController.php
new file mode 100644
index 0000000..60d80b8
--- /dev/null
+++ b/source/packages/com_mokosuitenpo/api/src/Controller/NpoReportsController.php
@@ -0,0 +1,123 @@
+getIdentity();
+ if (!$user || $user->guest || (!$user->authorise('core.admin') && !$user->authorise($action, 'com_mokosuitenpo'))) {
+ http_response_code(403);
+ echo json_encode(['error' => 'Access denied']);
+ Factory::getApplication()->close();
+ }
+ }
+
+ public function annualReport(): void
+ {
+ $this->requireAuth();
+ $year = Factory::getApplication()->getInput()->getInt('year', (int) date('Y'));
+ $summary = \Moko\Plugin\System\MokoSuiteNpo\Helper\ImpactReportHelper::getAnnualSummary($year);
+ $this->sendJson($summary);
+ }
+
+ public function donorImpact(): void
+ {
+ $this->requireAuth();
+ $input = Factory::getApplication()->getInput();
+ $impact = \Moko\Plugin\System\MokoSuiteNpo\Helper\ImpactReportHelper::getDonorImpact(
+ $input->getInt('donor_id', 0),
+ $input->getInt('year', (int) date('Y'))
+ );
+ $this->sendJson($impact);
+ }
+
+ public function listRecurring(): void
+ {
+ $this->requireAuth();
+ $db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
+
+ $db->setQuery($db->getQuery(true)
+ ->select('p.*, cd.name AS donor_name')
+ ->from($db->quoteName('#__mokosuitenpo_pledges', 'p'))
+ ->join('INNER', $db->quoteName('#__mokosuitenpo_donors', 'd') . ' ON d.id = p.donor_id')
+ ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = d.contact_id')
+ ->where($db->quoteName('p.status') . ' = ' . $db->quote('active'))
+ ->where('p.saved_payment_id IS NOT NULL')
+ ->order('p.next_charge_date ASC'));
+
+ $this->sendJson($db->loadObjectList() ?: []);
+ }
+
+ public function createRecurring(): void
+ {
+ $this->requireAuth('npo.donations');
+ $input = Factory::getApplication()->getInput();
+
+ $id = \Moko\Plugin\System\MokoSuiteNpo\Helper\RecurringDonationHelper::createPledge(
+ $input->getInt('donor_id', 0),
+ $input->getFloat('amount', 0),
+ $input->getString('frequency', 'monthly'),
+ $input->getInt('saved_payment_id', 0),
+ $input->getInt('fund_id', 0) ?: null
+ );
+
+ $this->sendJson(['success' => true, 'pledge_id' => $id]);
+ }
+
+ public function cancelRecurring(): void
+ {
+ $this->requireAuth('npo.donations');
+ $id = Factory::getApplication()->getInput()->getInt('id', 0);
+
+ $db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
+
+ // Verify pledge exists and is active before cancelling
+ $db->setQuery($db->getQuery(true)
+ ->select('id, status')
+ ->from('#__mokosuitenpo_pledges')
+ ->where('id = ' . (int) $id));
+ $pledge = $db->loadObject();
+
+ if (!$pledge) {
+ http_response_code(404);
+ $this->sendJson(['success' => false, 'error' => 'Pledge not found']);
+ return;
+ }
+ if ($pledge->status !== 'active') {
+ $this->sendJson(['success' => false, 'error' => 'Pledge is not active']);
+ return;
+ }
+
+ $db->setQuery($db->getQuery(true)
+ ->update('#__mokosuitenpo_pledges')
+ ->set($db->quoteName('status') . ' = ' . $db->quote('cancelled'))
+ ->set($db->quoteName('cancelled_at') . ' = ' . $db->quote(Factory::getDate()->toSql()))
+ ->where('id = ' . (int) $id)
+ ->where($db->quoteName('status') . ' = ' . $db->quote('active')));
+ $db->execute();
+
+ $this->sendJson(['success' => true]);
+ }
+
+ private function sendJson(mixed $data): void
+ {
+ header('Content-Type: application/json; charset=utf-8');
+ echo json_encode($data, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
+ Factory::getApplication()->close();
+ }
+}
diff --git a/source/packages/com_mokosuitenpo/mokosuitenpo.xml b/source/packages/com_mokosuitenpo/mokosuitenpo.xml
index 4135fa0..f291a57 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.01.00
+ 01.01.01
8.3
MokoSuite NPO component
Moko\Component\MokoSuiteNpo
diff --git a/source/pkg_mokosuitenpo.xml b/source/pkg_mokosuitenpo.xml
index dbef4db..0758b55 100644
--- a/source/pkg_mokosuitenpo.xml
+++ b/source/pkg_mokosuitenpo.xml
@@ -2,7 +2,7 @@
Package - MokoSuite NPO
mokosuitenpo
- 01.01.00
+ 01.01.01
2026-06-11
Moko Consulting
hello@mokoconsulting.tech