# Moon ERP Accounting Module — Compliance Audit

**Scope**: `/home/moonerpelbaset/moon_erp/Modules/Accounting/` (backend), `/home/moonui/public_html/moon-erp/src/app/` (frontend).
**Date**: 2026-05-11. **Auditor**: Legal-grade compliance review (Saudi/SOCPA/ZATCA context).

> Architecture is a Laravel modular structure with `Actions/`, `Services/`, `Models/`, `Http/{Controllers,Requests,Resources}/`, `Enums/`, and Eloquent migrations. Code shows above-average craftsmanship for AI-generated output — but several legal-grade gaps exist.

---

## VERDICT SUMMARY

| Severity | Count | Examples |
|---|---|---|
| CRITICAL | 7 | No ZATCA e-invoicing, no audit trail, period-lock bypass, balance not enforced at DB, segregation-of-duties absent, reopen year breaks immutability, no withholding tax handling, no zakat |
| HIGH    | 9 | Header-account posting unchecked, decimal(15,3) instead of SAR halala, no FX revaluation, no Hijri date, reversal can target closed period, no trial-balance hard check, no provisions/bad-debt, balance sheet missing interim net income, cash flow not IAS-7 |
| MEDIUM  | 8 | Rounding via `round()` not banker's, no concurrency lock on numbering, no approval thresholds, header/detail toggle automatic, no comparative period preservation, AR aging double-count risk, no current/non-current sub-type, code reuse after delete |
| LOW     | 5 | Documentation thin for accountants, no ERD, OpenAPI partial, missing `created_by` on lines, `entry_number` nullable forever |

---

## SECTION A — FUNDAMENTAL ACCOUNTING PRINCIPLES

### 1. Double-entry bookkeeping  — HIGH
`JournalEntryService::validateBalance()` uses `bccomp` correctly. `ApproveJournalEntry` calls `isBalanced()`. **BUT**:
- `StoreJournalEntryRequest` does NOT validate balance — only `min:2` lines. POST with unbalanced lines lands in `draft`.
- DB has **no constraint** ensuring `total_debit = total_credit` or that lines sum to header totals. Direct SQL UPDATE or buggy path can desync silently.
- `CreateJournalEntry` writes `total_debit`/`total_credit` from input and never re-computes from saved lines.
Fix: DB CHECK or post-save recompute. Reject unbalanced drafts at request validation.

### 2. Accrual basis — LOW
Accrual is supported (recurring entries, depreciation, AR/AP via opening-balance invoices). Acceptable.

### 3. Going concern / year-end carryover — MEDIUM
`ExecuteYearEndClosing` correctly closes revenue/expense → income summary → retained earnings → next-year opening-balance entry. **Gap**: opening-balance entry into next year requires next-year `FiscalYear` to already exist; otherwise balances are silently dropped (no exception thrown).

### 4. Consistency — HIGH
`UpdateAccountRequest` correctly omits `code`, `classification`, `nature`, `account_type`. **But** no test enforces this; `is_system` flag is a fillable column on the model and can be flipped via mass-assignment in unrelated code paths.

### 5. Matching principle / depreciation — HIGH
`FixedAssetService::calculateMonthlyDepreciation` supports straight-line, declining, accelerated. **`Accelerated == StraightLine` in code** — labeling bug. No prepaid/accrued framework beyond manual entries; no deferred-revenue/expense schedule.

### 6. Conservatism — HIGH
- No FX revaluation routine for unrealized FX gain/loss on monetary FX balances at period end. SOCPA/IFRS requires it.
- No bad-debt provision / allowance for doubtful accounts module. AR aging exists but no automated provisioning workflow.

### 7. Materiality / rounding — MEDIUM
Everywhere uses `round($x, 3)` (half-away-from-zero). Bookkeeping standards prefer banker's rounding for VAT-line aggregation. SAR halala precision is **2 decimals**; DB uses `decimal(15,3)` — risks UI/DB mismatch and sub-halala drift.

### 8. Audit trail — CRITICAL
**No activity log**. `grep` for `spatie/laravel-activitylog`, `LogsActivity`, `activity()->` in Accounting → **0 hits**.
- Only `created_by`, `approved_by`, `posted_by` on journal_entries — three attribution points only.
- `JournalEntryLine` has **no `created_by`**.
- Draft `update()` replaces fields silently.
- `Account` updates have no before/after log; `is_system` can be flipped.
SOCPA / ZATCA both require a full audit trail. ZATCA Phase 2 requires cryptographic invoice-level immutability. **Legal-grade failure**.

---

## SECTION B — CHART OF ACCOUNTS

### 9. Account types — MEDIUM
`AccountClassification` covers Assets, Liabilities, Equity, Revenue, Expenses. `AccountType` distinguishes header/detail. Current/non-current sub-types are NOT modeled — only inferred from parent hierarchy. For SOCPA-compliant SFP, current/non-current should be a column on the account.

### 10. Normal balance — LOW (strength)
`AccountClassification::nature()` returns correct DR/CR. Reports apply nature correctly.

### 11. Account hierarchy / control accounts — HIGH
`AccountController::store` auto-flips parent to `account_type=header` and `has_children=true`. **Gap**: nothing checks whether the parent has posted entries — a detail account with history can silently become a header. Also `StoreJournalEntryRequest` does NOT check that `account_id` is `account_type=detail` and `status=active`. Header accounts can be posted to.

### 12. Account codes — MEDIUM
Unique on `[company_id, code]`. `AccountCodeGenerator::clearSoftDeletedCode` force-deletes soft-deleted codes on collision → **code reuse after delete is allowed by design**. Auditors typically require code permanence.

---

## SECTION C — SAUDI-SPECIFIC

### 13. VAT (15%) — MEDIUM
`TaxRate` + `TaxService::calculateTax` math is correct for inclusive/exclusive. **Gaps**:
- No VAT return / filing report endpoint.
- No separation between Input VAT (recoverable) and Output VAT (payable) at system level — single `account_id` per tax rate.
- No VAT period (monthly/quarterly) reporting.

### 14. ZATCA e-invoicing — CRITICAL
`grep -ri "zatca|e-invoice|einvoice|fatoorah"` → **zero matches**. No XML invoice generation, no UBL 2.1, no QR code TLV base64, no CSID, no clearance/reporting integration. **Deal-breaker for any Saudi commercial deployment.**

### 15. Arabic support / RTL / Hijri — MEDIUM
`name_ar`, `description_ar` columns present and required. RTL is implemented in frontend. **Hijri date support absent** — `grep` for `hijri|umalqura` → 0 hits. SOCPA statements often require dual-date display.

### 16. SAR precision — HIGH
Money columns are `decimal(12,3)` or `decimal(15,3)`. SAR halala = 1/100 SAR (2 dp). Using 3 dp risks display mismatch, sub-halala drift after FX conversion (rates stored at 6 dp), and ZATCA totals require 2 dp.

### 17. Withholding tax — CRITICAL
`TaxType::Withholding` enum exists. **But**: no `WithholdingTaxService`, no payment-voucher integration auto-deducting WHT, no liability account auto-posting, no certificate workflow, no filing report. Mandatory for foreign-vendor payments at 5%/15%/20%.

### 18. Zakat — CRITICAL
`grep -ri "zakat"` → **zero matches**. No zakat base computation, no adjusted equity calculation, no GAZT/ZATCA declaration. Mandatory for Saudi-owned entities.

---

## SECTION D — INTERNAL CONTROLS

### 19. Segregation of duties — CRITICAL
`ApproveJournalEntry` and `PostJournalEntry` accept `$userId` from the caller but never check `$userId !== $entry->created_by`. Same user can create, approve, post. Auto-post setting actively encourages this. No policy/middleware enforces SoD.

### 20. Approval workflows / thresholds — MEDIUM
Permission middleware exists (`accounting.journal-entries.approve`, `.post`). No amount thresholds, no multi-tier (manager/CFO) approval, no escalation. `ApprovalWorkflow` is absent from the Accounting module despite existing elsewhere.

### 21. Period locking — HIGH
`PostJournalEntry` checks `fiscalPeriod->isOpen()`. **But**:
- Draft entry `update()` has no period check — a draft created when period was open can be edited after the period closes if it stays draft.
- `CloseFiscalPeriod` only blocks unposted entries — orphan drafts pile up.
- `ReverseJournalEntry` accepts user-supplied date; `resolvePeriod` enforces an open period exists, but no docs warn the accountant.

### 22. Year-end irreversibility — CRITICAL
`ReopenFiscalYear` cancels closing JEs and flips status back to Open. No "finalized" terminal state. After external audit sign-off this is unacceptable — comparatives can be altered after the fact. No comparative-period snapshot preserved.

### 23. Reconciliation enforcement — MEDIUM
`BankReconciliation` schema has `difference`. `CompleteReconciliation` action exists; an enforced `difference == 0` check before completion was not located — needs explicit confirmation.

### 24. Trial-balance hard-check — HIGH
No automated job asserts TB equality on a schedule or before/after period close. Period-close performs no TB-equality check.

---

## SECTION E — FINANCIAL REPORTING

### 25. Trial balance — LOW (strength)
`GeneralLedgerService::getTrialBalance` exists, served by `LedgerController`.

### 26. General Ledger — LOW (strength)
Opening balance, entries with running balance, cost-center filter, date range. Solid.

### 27. Income Statement — LOW (strength)
Correctly nets revenue minus expenses with proper nature application. Filters by date range, cost center.

### 28. Balance Sheet — HIGH
`BalanceSheetService::generate` returns Assets, Liabilities, Equity. **Does NOT include current-period net income** as an equity line. Until year-end closing runs, the BS will NOT balance for any interim date. Common reporting bug — must add a "current period earnings" plug line.

### 29. Cash flow statement — MEDIUM
Self-labeled "simplified" in docblock. Uses net-income proxy + non-current asset/equity changes. **Not IFRS-compliant indirect method**: omits depreciation add-back, working-capital changes (AR/AP/inventory), proper classification of interest/tax. Would not pass external audit.

### 30. Aging (AR/AP) — MEDIUM
Standard buckets (current, 30, 60, 90, 120+). Combines opening-balance invoices and JE-line balances — risk of double-counting if an opening-balance invoice has subsequent JE postings.

---

## SECTION F — DOCUMENTATION

### 31. User documentation — LOW
`docs/MODULE.md` exists with endpoint list. No accountant SOPs for month-end / year-end / reconciliation.

### 32. API documentation — LOW (strength)
Scribe annotations on controllers; OpenAPI spec is published per project memory.

### 33. Schema / ERD — LOW
No ERD. Schema documented only via migration files.

---

## STRENGTHS

1. Clean Action-pattern architecture; each state transition has a single-responsibility class.
2. BCMath used for balance checks — avoids float-comparison errors.
3. Multi-currency line storage with original + base-currency equivalents.
4. Sequence service assigns entry numbers only at post time — prevents gap-from-cancellation.
5. Soft deletes everywhere.
6. Permission middleware on every controller action.
7. Reversal pattern is correct (new opposing entry, not mutation).
8. `is_system` flag protects system accounts from deletion.
9. Auto-post is a per-company setting, not hard-wired.
10. Multi-tenant `company_id` scoping is consistent.

---

## TOP PRIORITY FIXES (ordered)

1. Add full audit log (spatie/laravel-activitylog) to Accounts, JournalEntry, JournalEntryLine, FiscalPeriod, FiscalYear, TaxRate, ExchangeRate, BankReconciliation. **CRITICAL**
2. Implement ZATCA Phase 2 (UBL 2.1 XML, QR TLV, CSID, clearance API) in a dedicated EInvoicing module. **CRITICAL**
3. Enforce SoD — reject approval where `approver_id == created_by`. Reject post where `poster_id == approver_id` for high-value entries. **CRITICAL**
4. Add zakat & withholding tax sub-modules with proper liability accounts and filing reports. **CRITICAL**
5. Add DB-level balance constraint or post-save recompute. **CRITICAL**
6. Remove `ReopenFiscalYear` for non-admins / add "finalized" terminal status with no reopen path. **CRITICAL**
7. Add FX revaluation routine for monetary FX balances at period end. **HIGH**
8. Fix Balance Sheet to include current-period net income line until year-end close. **HIGH**
9. Rewrite cash flow as true indirect method per IAS 7. **HIGH**
10. SAR precision: enforce 2 dp at service layer using `currencies.decimals`; migrate SAR columns to `decimal(15,2)`. **HIGH**
11. Validate `account_type=detail` and `status=active` for every JE line account before posting. **HIGH**
12. Add Hijri date dual support for Saudi statements. **MEDIUM**
