From 502dfa40d9d15a27f4d3d748af5892ef51257506 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 20 Jun 2026 17:18:54 -0500 Subject: [PATCH 1/4] =?UTF-8?q?Add=20FundAccountingHelper=20=E2=80=94=20re?= =?UTF-8?q?stricted/unrestricted=20fund=20balances,=20GAAP=20compliance,?= =?UTF-8?q?=20expense=20ratios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Helper/FundAccountingHelper.php | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 source/packages/plg_system_mokosuitenpo/src/Helper/FundAccountingHelper.php diff --git a/source/packages/plg_system_mokosuitenpo/src/Helper/FundAccountingHelper.php b/source/packages/plg_system_mokosuitenpo/src/Helper/FundAccountingHelper.php new file mode 100644 index 0000000..a74d6a3 --- /dev/null +++ b/source/packages/plg_system_mokosuitenpo/src/Helper/FundAccountingHelper.php @@ -0,0 +1,126 @@ +get(DatabaseInterface::class); + + $db->setQuery($db->getQuery(true) + ->select('f.id, f.name, f.type, f.description') + ->select('COALESCE((SELECT SUM(d.amount) FROM #__mokosuitenpo_donations d WHERE d.fund_id = f.id), 0) AS total_received') + ->select('COALESCE((SELECT SUM(e.amount) FROM #__mokosuitenpo_fund_expenses e WHERE e.fund_id = f.id), 0) AS total_spent') + ->from($db->quoteName('#__mokosuitenpo_funds', 'f')) + ->where($db->quoteName('f.status') . ' = ' . $db->quote('active')) + ->order('f.type ASC, f.name ASC')); + + $funds = $db->loadObjectList() ?: []; + + foreach ($funds as &$f) { + $f->balance = round((float) $f->total_received - (float) $f->total_spent, 2); + $f->is_restricted = ($f->type === 'restricted' || $f->type === 'temporarily_restricted'); + } + + return $funds; + } + + /** + * Record an expense against a fund. + */ + public static function recordExpense(int $fundId, float $amount, string $description, string $category = 'program'): int + { + $db = Factory::getContainer()->get(DatabaseInterface::class); + $now = Factory::getDate()->toSql(); + + // Verify fund exists and has sufficient balance + $db->setQuery($db->getQuery(true)->select('type')->from('#__mokosuitenpo_funds')->where('id = ' . (int) $fundId)); + $fundType = $db->loadResult(); + + if (!$fundType) throw new \RuntimeException('Fund not found'); + + $expense = (object) [ + 'fund_id' => $fundId, + 'amount' => $amount, + 'description' => $description, + 'category' => $category, // program, admin, fundraising + 'recorded_by' => Factory::getApplication()->getIdentity()->id, + 'recorded_at' => $now, + ]; + + $db->insertObject('#__mokosuitenpo_fund_expenses', $expense, 'id'); + return (int) $expense->id; + } + + /** + * Get Statement of Financial Position (nonprofit balance sheet). + */ + public static function getFinancialPosition(): object + { + $funds = self::getFundBalances(); + + $unrestricted = 0; + $tempRestricted = 0; + $permRestricted = 0; + + foreach ($funds as $f) { + switch ($f->type) { + case 'unrestricted': $unrestricted += $f->balance; break; + case 'temporarily_restricted': $tempRestricted += $f->balance; break; + case 'permanently_restricted': case 'restricted': $permRestricted += $f->balance; break; + } + } + + return (object) [ + 'unrestricted' => $unrestricted, + 'temporarily_restricted' => $tempRestricted, + 'permanently_restricted' => $permRestricted, + 'total_net_assets' => $unrestricted + $tempRestricted + $permRestricted, + 'fund_count' => count($funds), + ]; + } + + /** + * Get expense breakdown by category (program vs admin vs fundraising). + */ + public static function getExpenseBreakdown(string $from = '', string $to = ''): object + { + $db = Factory::getContainer()->get(DatabaseInterface::class); + $from = $from ?: date('Y-01-01'); + $to = $to ?: date('Y-12-31'); + + $db->setQuery($db->getQuery(true) + ->select('category') + ->select('COUNT(*) AS count, COALESCE(SUM(amount), 0) AS total') + ->from('#__mokosuitenpo_fund_expenses') + ->where('DATE(recorded_at) BETWEEN ' . $db->quote($from) . ' AND ' . $db->quote($to)) + ->group('category')); + + $byCategory = $db->loadObjectList('category') ?: []; + + $program = (float) ($byCategory['program']->total ?? 0); + $admin = (float) ($byCategory['admin']->total ?? 0); + $fundraising = (float) ($byCategory['fundraising']->total ?? 0); + $totalExpenses = $program + $admin + $fundraising; + + return (object) [ + 'program' => $program, + 'admin' => $admin, + 'fundraising' => $fundraising, + 'total' => $totalExpenses, + 'program_pct' => $totalExpenses > 0 ? round($program / $totalExpenses * 100, 1) : 0, + 'overhead_pct' => $totalExpenses > 0 ? round(($admin + $fundraising) / $totalExpenses * 100, 1) : 0, + ]; + } +} -- 2.52.0 From 9af651d2be80c97da63e8012f0718838ae527e55 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sat, 20 Jun 2026 22:19:48 +0000 Subject: [PATCH 2/4] chore(version): auto-bump patch 01.02.01-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- source/packages/com_mokosuitenpo/mokosuitenpo.xml | 2 +- source/pkg_mokosuitenpo.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index c2b02a6..c71112b 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: moko-platform.Automation -# VERSION: 01.00.00 +# VERSION: 01.02.01 # 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 cb571de..4bb74db 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.02.00 + 01.02.01 8.3 MokoSuite NPO component Moko\Component\MokoSuiteNpo diff --git a/source/pkg_mokosuitenpo.xml b/source/pkg_mokosuitenpo.xml index ad69e32..352a537 100644 --- a/source/pkg_mokosuitenpo.xml +++ b/source/pkg_mokosuitenpo.xml @@ -2,7 +2,7 @@ Package - MokoSuite NPO mokosuitenpo - 01.02.00 + 01.02.01 2026-06-11 Moko Consulting hello@mokoconsulting.tech -- 2.52.0 From c5c492463edbbd41c8cd9ea35355300f70377f80 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 20 Jun 2026 17:28:00 -0500 Subject: [PATCH 3/4] fix: enforce restricted fund balance check before recording expenses (GAAP) --- .../src/Helper/FundAccountingHelper.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/source/packages/plg_system_mokosuitenpo/src/Helper/FundAccountingHelper.php b/source/packages/plg_system_mokosuitenpo/src/Helper/FundAccountingHelper.php index a74d6a3..6f29510 100644 --- a/source/packages/plg_system_mokosuitenpo/src/Helper/FundAccountingHelper.php +++ b/source/packages/plg_system_mokosuitenpo/src/Helper/FundAccountingHelper.php @@ -50,6 +50,19 @@ class FundAccountingHelper if (!$fundType) throw new \RuntimeException('Fund not found'); + // Enforce balance check on restricted funds (GAAP compliance) + if ($fundType === 'restricted' || $fundType === 'temporarily_restricted') { + $db->setQuery($db->getQuery(true) + ->select('COALESCE((SELECT SUM(d.amount) FROM #__mokosuitenpo_donations d WHERE d.fund_id = ' . (int) $fundId . '), 0)' + . ' - COALESCE((SELECT SUM(e.amount) FROM #__mokosuitenpo_fund_expenses e WHERE e.fund_id = ' . (int) $fundId . '), 0) AS balance') + ->from('DUAL')); + $balance = (float) $db->loadResult(); + + if ($amount > $balance) { + throw new \RuntimeException('Insufficient balance in restricted fund (available: $' . number_format($balance, 2) . ', requested: $' . number_format($amount, 2) . ')'); + } + } + $expense = (object) [ 'fund_id' => $fundId, 'amount' => $amount, -- 2.52.0 From fbccca11bb3d67685c919da1cb1c6b3dd63a8489 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sat, 20 Jun 2026 22:29:10 +0000 Subject: [PATCH 4/4] chore(version): auto-bump patch 01.02.02-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- source/packages/com_mokosuitenpo/mokosuitenpo.xml | 2 +- source/pkg_mokosuitenpo.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index c71112b..d87ce9e 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: moko-platform.Automation -# VERSION: 01.02.01 +# VERSION: 01.02.02 # 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 4bb74db..9c8cd31 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.02.01 + 01.02.02 8.3 MokoSuite NPO component Moko\Component\MokoSuiteNpo diff --git a/source/pkg_mokosuitenpo.xml b/source/pkg_mokosuitenpo.xml index 352a537..77ac8cc 100644 --- a/source/pkg_mokosuitenpo.xml +++ b/source/pkg_mokosuitenpo.xml @@ -2,7 +2,7 @@ Package - MokoSuite NPO mokosuitenpo - 01.02.01 + 01.02.02 2026-06-11 Moko Consulting hello@mokoconsulting.tech -- 2.52.0