# MedAccounting — General Ledger / Chart of Accounts / Journals (SIFA plan P7-T2)

Medical accounting for the Shifa One platform. **Ground rule: bind to the existing
ERP financial data — extend, never rebuild.** The ERP already keeps a
**payment-level double-entry** ledger (`app/Account` + `app/AccountTransaction`,
posted by `app/Utils/AccountTransactionUtil` whenever a payment lands on a
cash/bank account). MedAccounting does **not** touch that. It adds the
**General Ledger layer on top**: a full chart of accounts, balanced journal
entries with cost centres and period locks, an auto-posting engine that derives
GL entries from existing ERP events, and the GL reports.

No dependency on ClinicCore, NeoSkin or TurboBoost (`module.json` → `requires: []`).

---

## How it binds vs. what it adds

| Layer | Owner | MedAccounting's relationship |
|---|---|---|
| Cash/bank register + payment-level entries | ERP `accounts` / `account_transactions` (`AccountTransactionUtil`) | **read-only binding** — never mutated |
| Invoices / payments / purchases / expenses / payroll | ERP `transactions` / `transaction_payments` | **read-only source** for auto-posting |
| Chart of accounts (hierarchical GL) | **new** `medacc_accounts` | added on top |
| Journal entries + lines (draft/posted/reversed) | **new** `medacc_journal_entries` / `medacc_journal_lines` | added on top |
| Cost centres | **new** `medacc_cost_centers` (binds `clc_departments` / `business_locations` via `ref_id`) | added on top |
| Accounting periods (open/close lock) | **new** `medacc_periods` | added on top |

The core `account_transactions` table stays the **cash/bank sub-ledger** the POS
pays into; the new `medacc_*` tables are the **full accrual GL** posted from the
same underlying `transactions`. The two are complementary, not duplicated:
`account_transactions` records *"money moved on this account when a payment
happened"*; the GL records *"here is the balanced accrual journal (AR, revenue,
tax, AP, expense, payroll) for every business event."*

All tables are `business_id`-scoped and prefixed `medacc_`. Migrations dated
`2026_07_06_00xx`. References into ERP / ClinicCore tables are plain indexed
columns (no cross-DB FK constraints), matching the module convention.

---

## Tables

1. **medacc_accounts** — chart of accounts: `code` (hierarchical, unique/business),
   `name`, `type` (asset/liability/equity/revenue/expense), `sub_type`,
   `parent_id` (self-FK), `is_active`, `is_system` (seeded defaults), `description`.
2. **medacc_journal_entries** — `entry_no` (JE-{biz}-{seq}), `date`, `description`,
   `reference_type` (invoice/payment/payroll/purchase/expense/manual) + `reference_id`
   (idempotency key), `status` (draft/posted/reversed), posted/reversed audit
   columns, `reversal_of_id`, `total_debit`, `total_credit`, `created_by`.
3. **medacc_journal_lines** — `journal_entry_id` (cascade), `account_id`, `debit`,
   `credit`, `description`, `cost_center_id`.
4. **medacc_cost_centers** — `name`, `code`, `type` (department/facility/project/other),
   `ref_id` (links `clc_departments` / `business_locations`), `is_active`.
5. **medacc_periods** — `name`, `start_date`, `end_date`, `status` (open/closed),
   closed audit columns. A closed period blocks backdated posting.
6. **permissions migration** — `medacc.coa.manage`, `medacc.journal.manage`,
   `medacc.journal.post`, `medacc.periods.manage`, `medacc.reports.view`,
   `medacc.costcenters.manage`.

---

## Services

### LedgerService — the balanced-posting engine
- `postEntry($header, $lines)` — validates balanced (Σdebit == Σcredit, > 0),
  every account belongs to the business, and the date is **not inside a closed
  period**, then writes a posted entry + lines atomically.
- `reverse($entry)` — posts a mirror entry (debits↔credits swapped) referencing the
  original and stamps it `reversed` (posted entries are never mutated — audit trail).
- `balance()` / `accountBalance()` — the shared aggregation helpers used by the reports.

### AutoPostingService — ERP-event → balanced-journal mapper (the binding)
Reads the ERP `transactions` / `transaction_payments` and derives a balanced entry
per event, **idempotent per `(reference_type, reference_id)`**. It never hooks core
models — posting is call-on-demand plus the backfill command, so the core stays
byte-identical. Fail-soft: a missing account/table/event is skipped (logged), not
thrown.

| ERP event | Journal entry | COA codes (default) |
|---|---|---|
| Patient/insurance **payment** (`transaction_payments`) | **Dr** Cash `1100` *or* Bank `1110` (by `method`) · **Cr** AR `1200` | 1100/1110, 1200 |
| **Sell / invoice** finalised (`transactions` type=sell) | **Dr** AR `1200` · **Cr** Revenue `4000` · **Cr** Tax Payable `2200` (when taxed) | 1200, 4000, 2200 |
| **Purchase** (type=purchase) | **Dr** Inventory `1300` · **Dr** Input Tax `2200` · **Cr** AP `2100` | 1300, 2200, 2100 |
| **Expense** (type=expense) | **Dr** Expense `5900` · **Cr** AP `2100` | 5900, 2100 |
| **Payroll** (type=payroll) | **Dr** Salary Expense `5100` · **Cr** Payroll Payable `2300` | 5100, 2300 |

> On this ERP, **payroll is a `transactions` row** (`type='payroll'`, gross in
> `final_total`, `essentials_deductions` JSON) — not a separate `essentials_payrolls`
> table (that was migrated into `transactions` long ago). AutoPostingService reads
> those rows, guarded with `Schema::hasTable`/`hasColumn`.

`medacc:sync-postings {--since=} {--business=}` scans recent ERP events and posts
any missing entries (idempotent; safe to re-run). Skips a business with no chart of
accounts with a warning.

---

## Reports (medacc.reports.view)

Plain aggregate queries over **posted** journal lines — no heavy libs. Each has a
date/period filter.

- **Trial Balance** — every account's net debit/credit; total debit must == total credit.
- **General Ledger** — one account's posted lines with opening + running balance.
- **Income Statement** — revenue − expense for the period.
- **Balance Sheet** — assets == liabilities + equity (as of a date; net income to
  date shown as retained earnings inside equity).
- **Departmental / Cost-centre P&L** — revenue/expense per cost centre.

---

## Seeders

- **MedaccPermissionSeeder** — registers the 6 `medacc.*` permissions (idempotent).
- **MedaccChartSeeder** — the standard medical COA (`is_system=true`), seeded per
  business, **only when the business has none** (never fights a customised COA).
  Codes match `config('medaccounting.accounts')` so every auto-posting mapping resolves.
- **MedaccDemoSeeder** — guarded on the **'Shifa One'** business but **falls back
  gracefully**: if absent it logs a skip via `$this->command->warn` (not a silent
  return). When present: seeds the COA, opens the current period, and runs
  `AutoPostingService::backfill` over the business's existing ERP sells/payments so
  the demo trial balance is non-empty (reads existing ERP data — never invents any).

---

## UI map (routes under `med-accounting`, `medacc.*`)

| Screen | Route name | Permission |
|---|---|---|
| Chart of Accounts CRUD | `medacc.coa.*` | `medacc.coa.manage` |
| Journal Entries (list / post / view / reverse) | `medacc.journal.*` | `medacc.journal.manage` (+ `medacc.journal.post` to post/reverse) |
| Cost Centers CRUD | `medacc.costcenters.*` | `medacc.costcenters.manage` |
| Periods (open/close) | `medacc.periods.*` | `medacc.periods.manage` |
| Reports (5 GL reports) | `medacc.reports.*` | `medacc.reports.view` |

Menu injected by `DataController::modifyAdminMenu()` under **"Accounting"**
(order ~46), gated by the SaaS entitlement helper (guarded with
`function_exists('module_entitled')` so single-tenant hosts default to allowed) +
the `medaccounting_module` subscription flag + the `medacc.*` permissions — exactly
like MedBilling.

---

## Install / permissions

Standard superadmin install/update/repair endpoints
(`/med-accounting/install|update|repair`), gated by the lowercase
`medaccounting_version` System property (`ModuleUtil::isModuleInstalled`).

Permissions granted to `Admin` on install. Superadmin package flag:
`medaccounting_module`.

---

## Deviations flagged (Ground Rule 6)

1. **GL is a separate accrual ledger, not the core `account_transactions`.** The
   ERP's `account_transactions` is a payment-level cash/bank sub-ledger; a full
   accrual GL (AR/revenue/tax/AP/accrued payroll) cannot be reconstructed from it
   alone, so the GL is posted from the underlying `transactions`. The two coexist;
   the core ledger is never modified.
2. **Payroll deduction split simplified.** The base payroll mapping credits the
   whole gross to Payroll Payable (`2300`); a future split of tax/deduction
   sub-payables can read `transactions.essentials_deductions`. Flagged, not blocking.
3. **Refunds / returns not auto-posted in the base mapping.** `postPayment` skips
   zero/negative amounts and `is_return` payments are excluded from the backfill —
   return handling is a later increment. Manual reversing entries cover them today.
4. **`modules_statuses.json`** gains a `"MedAccounting": true` entry — the standard
   nwidart module-registration requirement (peer modules are registered the same
   way). No other file outside `Modules/MedAccounting` is touched.
