# Accounting Module Audit Report
**Project:** Ziad Laravel / React Template  
**Date:** 2026-04-06  
**Auditor:** Claude Code (claude-sonnet-4-6)  
**Scope:** Full-stack — `FinanceController.php`, `BalanceSheet.jsx`, `IncomeStatement.jsx`, `Ledger.jsx`  
**Estimated Discrepancy:** ~2.24 Billion Rupiah

---

## Table of Contents

1. [Executive Summary](#1-executive-summary)
2. [System Architecture Overview](#2-system-architecture-overview)
3. [Domain Knowledge Reference](#3-domain-knowledge-reference)
4. [Finding 1 — COA Misclassification (Phase 1)](#4-finding-1--coa-misclassification-phase-1)
5. [Finding 2 — Flawed Aggregation Logic (Phase 2)](#5-finding-2--flawed-aggregation-logic-phase-2)
6. [Finding 3 — Split-Brain Architecture (Phase 3)](#6-finding-3--split-brain-architecture-phase-3)
7. [Finding 4 — Frontend Math Offloading (Phase 4)](#7-finding-4--frontend-math-offloading-phase-4)
8. [Finding 5 — Frontend Independent Bugs](#8-finding-5--frontend-independent-bugs)
9. [Why Balanced Clients Do Not Disprove These Findings](#9-why-balanced-clients-do-not-disprove-these-findings)
10. [Audit Tool Flaw — RecalculateLedger Command](#10-audit-tool-flaw--recalculateledger-command)
11. [Full Bug Summary Table](#11-full-bug-summary-table)
12. [Recommended Fix Order](#12-recommended-fix-order)

---

## 1. Executive Summary

The Ziad financial reporting module contains a layered set of accounting logic errors spanning both the backend API and the frontend UI. These errors compound each other, producing a Balance Sheet (Neraca) and Income Statement (Laba Rugi) that are structurally incapable of being correct under any real-world data condition that includes accounting corrections, refunds, or journal reversals.

The root causes are:

- **Live database corruption:** Cash (Kas) accounts are misclassified as liabilities (`UTANG`) instead of assets (`HARTA`) in the `coas` table, directly inverting their sign on the Balance Sheet.
- **Partial-sum aggregation:** Both `getTotalPendapatan` and `getTotalBiaya` on the backend sum only one side of the ledger (credit-only for revenue, debit-only for expenses), silently dropping all accounting corrections.
- **Split-brain data sourcing:** The Balance Sheet reads from the `journals` table (the official ledger) while operational income/expense views read from the `transactions` table (the business event log). Due to confirmed historical race conditions, these two tables are out of sync.
- **Frontend math offloading with bugs:** The API exposes raw `debit` and `credit` columns. The frontend is expected to compute net balances itself, and it does so incorrectly in multiple places — independently of the backend errors.

The ~2.24 billion Rupiah discrepancy is the cumulative output of all four layers failing simultaneously. Some clients appear to have balanced reports only because their ledgers lack correction entries — the bugs are latent and will surface for every client over time.

---

## 2. System Architecture Overview

### Database Tables

| Table | Purpose |
|---|---|
| `coas` | Chart of Accounts — defines account names, codes, and classification groups |
| `journals` | The official double-entry ledger — one row per debit or credit line |
| `transactions` | Business event log — one row per payment/transfer event |

### Key Relationships

```
transactions (1) ──> (N) journals
coas        (1) ──> (N) journals  [via coa_id]
```

The `journals` table is the **accounting source of truth**. The `transactions` table is the **operational event log**. They are linked via `transaction_id` on the journal row. Due to race conditions during recording, some transactions exist without corresponding journal entries.

### Journal Table Schema

```
journals
  id                bigint PK
  transaction_id    bigint (nullable, FK to transactions)
  transactionDate   date
  coa_id            integer (FK to coas)
  coa_name          string  (denormalized name at time of recording)
  tahun_id          integer
  debit             bigint
  credit            bigint
```

Note: `debit` and `credit` are stored as `bigint`. For any given journal line, one side will be non-zero and the other will be zero. Net balance per account is derived by aggregating all lines for that `coa_id`.

### COA Groups in Use

| Group | Accounting Type | Normal Balance |
|---|---|---|
| `HARTA` | Asset | Debit |
| `PIUTANG` | Receivable (Asset) | Debit |
| `UTANG` | Liability | Credit |
| `MODAL` | Equity | Credit |
| `PENDAPATAN` | Revenue | Credit |
| `BIAYA` | Expense | Debit |

---

## 3. Domain Knowledge Reference

The standard accounting net balance formula depends on account type:

**Assets and Expenses (normal balance = Debit):**
```sql
COALESCE(SUM(debit) - SUM(credit), 0) AS balance
```

**Liabilities, Equity, and Revenue (normal balance = Credit):**
```sql
COALESCE(SUM(credit) - SUM(debit), 0) AS balance
```

Any formula that sums only one side (e.g., `SUM(credit)` alone for a revenue account) is only accidentally correct when the other side is zero — i.e., when there have been no corrections, reversals, or refunds. In a live financial system this condition does not hold indefinitely.

---

## 4. Finding 1 — COA Misclassification (Phase 1)

### Classification

**Severity:** Critical  
**Layer:** Database (live data)  
**File:** `coas` table — not derivable from source code, requires a direct DB query

### Description

Based on the reported symptom ("Kas AM BAKSO", "Kas AM JAJANAN" appearing as massive negative liabilities on the frontend), one or more Cash (Kas) accounts have their `group` column set to `UTANG` (Liability) instead of `HARTA` (Asset).

### How It Causes the Symptom

The `getHartaLancar()` method filters on `coas.group IN ('HARTA', 'PIUTANG')`. The `getUtang()` method filters on `coas.group = 'UTANG'`.

A Kas account has a normal Debit balance — money flowing in is recorded as `Debit Kas`. When this account is misclassified as `UTANG`, it lands in the liability section, and the frontend computes its balance as `credit - debit` (the liability formula). Since the account is predominantly debit, the result is a large **negative liability** — which is the exact symptom reported.

### Mechanism (BalanceSheet.jsx line 466)
```jsx
// Frontend renders utang using credit - debit formula
{new Intl.NumberFormat().format(utang.credit - utang.debit)}
```

A Kas account with `debit = 500,000,000` and `credit = 0` would render as `-500,000,000` in the liabilities column.

### Investigation Required

Run the following query against the production database to confirm affected accounts:

```sql
SELECT id, name, code, `group`
FROM coas
WHERE name LIKE '%Kas%'
   OR id IN (120, 123, 124, 125, 126, 273, 274, 275);
```

Any row where a Kas/Cash account shows `group = 'UTANG'` is a confirmed instance of this bug.

### Impact

This is the only finding that is **not masked by clean data**. It causes incorrect numbers regardless of whether journal entries have corrections or not, because the misclassification affects which section of the Balance Sheet the account appears in and which sign formula is applied.

---

## 5. Finding 2 — Flawed Aggregation Logic (Phase 2)

### Classification

**Severity:** Critical  
**Layer:** Backend — `FinanceController.php`  
**Methods:** `getTotalPendapatan()` (line 313), `getTotalBiaya()` (line 295)

### Description

Both scalar aggregate methods compute gross one-sided sums instead of true net balances. All accounting corrections (refunds, reversals, credit notes) are silently dropped from the calculation.

### `getTotalBiaya` — Lines 295–311

```php
$allBiaya = DB::table('journals')
    ->join('coas', 'coas.id', '=', 'journals.coa_id')
    ->selectRaw('sum(journals.debit) as debit')   // ← only debit
    ->where('coas.group', 'BIAYA')
    ->groupBy('coa_id')
    ->pluck('debit')
    ->toArray();

return array_sum($allBiaya);
```

**Problem:** Only `sum(journals.debit)` is selected. If a vendor refund or expense reversal posts a Credit entry to a BIAYA account, that Credit is completely ignored. The formula should be `SUM(debit) - SUM(credit)` for expense accounts.

### `getTotalPendapatan` — Lines 313–329

```php
$allPendapatan = DB::table('journals')
    ->join('coas', 'coas.id', '=', 'journals.coa_id')
    ->selectRaw('sum(journals.credit) as credit')  // ← only credit
    ->where('coas.group', 'PENDAPATAN')
    ->groupBy('coa_id')
    ->pluck('credit')
    ->toArray();

return array_sum($allPendapatan);
```

**Problem:** Only `sum(journals.credit)` is selected. Revenue reversals or corrections that post a Debit to a PENDAPATAN account are dropped. The formula should be `SUM(credit) - SUM(debit)` for revenue accounts.

### `getLaba` — Line 331–334

```php
public function getLaba(Request $request)
{
    return $this->getTotalPendapatan($request) - $this->getTotalBiaya($request);
}
```

Laba inherits both errors. The returned profit figure is:
- **Revenue:** inflated by the gross amount of all unaccounted revenue reversals
- **Expenses:** deflated by the gross amount of all unaccounted expense credits
- **Net profit:** doubly inflated as a result

### Affected Grouped Methods

The same one-sided aggregation exists in the grouped list endpoints used for displaying line-item breakdowns:

| Method | Line | Returns |
|---|---|---|
| `getAllPendapatan` | 174 | `sum(debit)`, `sum(credit)` — raw, no net |
| `getAllBiaya` | 189 | `sum(debit)`, `sum(credit)` — raw, no net |
| `getBiaya` | 234 | `sum(debit)`, `sum(credit)` — raw, no net |

These return both columns, leaving the net calculation to the frontend — which itself has bugs (see Finding 5).

---

## 6. Finding 3 — Split-Brain Architecture (Phase 3)

### Classification

**Severity:** High  
**Layer:** Backend — `FinanceController.php`  
**Methods:** `getOperationalIncome()` (line 366), `getOperationalExpense()` (line 379), `getTransactionalEquity()` (line 392)

### Description

The Balance Sheet methods query the `journals` table (the ledger). The operational income/expense methods bypass the ledger entirely and query the `transactions` table directly. These two views of financial reality are structurally guaranteed to diverge whenever a transaction fails to produce journal entries.

### The Two Realities

| Method | Source Table | Filter Method |
|---|---|---|
| `getHartaLancar()` | `journals` | by `coas.group` |
| `getUtang()` | `journals` | by `coas.group` |
| `getModal()` | `journals` | by `coas.group` |
| `getOperationalIncome()` | `transactions` | by `transaction_type_value IN (2, 4, 6)` |
| `getOperationalExpense()` | `transactions` | by `transaction_type_value = 5` |
| `getTransactionalEquity()` | `transactions` | by `transaction_type_value = 13` |

### `getOperationalIncome` — Lines 366–377

```php
return DB::table('transactions')
    ->selectRaw('transaction_type_value, sum(amount) as total')
    ->whereIn('transaction_type_value', [2, 4, 6])
    ->groupBy('transaction_type_value')
    ->get();
```

This queries `amount` from the transactions log and classifies by `transaction_type_value`. It has no knowledge of the journal ledger. If a transaction was recorded in the `transactions` table but its journal entries were dropped (the confirmed race condition), this method counts the income while the Balance Sheet does not — producing different totals for the same time period.

### `getOperationalExpense` — Lines 379–390

```php
return DB::table('transactions')
    ->selectRaw('transaction_type_value, sum(amount) as total')
    ->where('transaction_type_value', 5)
    ->groupBy('transaction_type_value')
    ->get();
```

Same problem. Expense total is sourced from `transactions`, while the Balance Sheet uses `journals`. A transaction with `transaction_type_value = 5` that lacks journal entries will appear in the operational expense view but not in the ledger-based reports.

### Why They Diverge

The race condition causing dropped journal inserts means the `transactions` and `journals` tables have different row counts for the same business events. The divergence grows over time as more events are recorded under load. There is no mechanism in the current codebase to detect or reconcile these gaps.

---

## 7. Finding 4 — Frontend Math Offloading (Phase 4)

### Classification

**Severity:** High  
**Layer:** Backend API contract + Frontend  
**Affected Endpoints:** All grouped report endpoints

### Description

Every grouped report endpoint returns raw `debit` and `credit` column values per `coa_id`, requiring the client to compute the net balance. The API currently exposes:

```json
{
  "id": 1,
  "coa_name": "Kas Sekolah",
  "debit": 5000000,
  "credit": 200000
}
```

Instead of the authoritative net:
```json
{
  "coa_name": "Kas Sekolah",
  "balance": 4800000
}
```

### Affected Backend Methods

| Method | selectRaw Output |
|---|---|
| `getHartaLancar()` | `sum(debit) as debit, sum(credit) as credit` |
| `getHartaTidakLancar()` | `sum(debit) as debit, sum(credit) as credit` |
| `getUtang()` | `sum(debit) as debit, sum(credit) as credit` |
| `getModal()` | `sum(debit) as debit, sum(credit) as credit` |
| `getAllPendapatan()` | `sum(debit) as debit, sum(credit) as credit` |
| `getAllBiaya()` | `sum(debit) as debit, sum(credit) as credit` |
| `getBiaya()` | `sum(debit) as debit, sum(credit) as credit` |
| `getCoasLedger()` | `sum(debit) as debit, sum(credit) as credit` |

### Consequences

Offloading this calculation to clients creates multiple failure points:
- Different UI views may apply different formulas to the same data
- A new developer building a new view may use the wrong formula without any indication from the API
- Client-side integer overflow or type coercion (e.g. JavaScript's `parseInt`) can silently corrupt large values
- The correct formula (`debit - credit` vs `credit - debit`) depends on account type, which the client must independently know or infer

---

## 8. Finding 5 — Frontend Independent Bugs

### Classification

**Severity:** Critical (IncomeStatement), Major (BalanceSheet)  
**Layer:** Frontend — `IncomeStatement.jsx`, `BalanceSheet.jsx`

### Description

The frontend contains accounting math errors that are **independent** of the backend bugs. Even if the backend were corrected to return true net balance values, the frontend would still display incorrect numbers in several places.

---

### `IncomeStatement.jsx`

#### Bug 1 — Total Pendapatan only sums `.credit` (line 116)

```js
settotalPendapatan(
  response.data
    .map(pendapatan => (parseInt(pendapatan.credit)))
    .reduce((partialSum, a) => partialSum + a, 0)
)
```

The API returns both `debit` and `credit` columns. The frontend reads only `.credit`. This mirrors the backend bug — both layers independently drop the debit side for revenue. Even with a fixed backend that returns net values, this line would need to read the net field.

#### Bug 2 — Total Biaya only sums `.debit` (line 132)

```js
settotalBiaya(
  response.data
    .map(biaya => (parseInt(biaya.debit)))
    .reduce((partialSum, a) => partialSum + a, 0)
)
```

Same pattern — only `.debit` is used for expense totals.

#### Bug 3 — Per-row line items also show one side only (lines 308, 345)

```jsx
// Pendapatan rows
{new Intl.NumberFormat().format(pendapatan.credit)}

// Biaya rows
{new Intl.NumberFormat().format(biaya.debit)}
```

Individual account rows in the Income Statement also display only the single raw column, not the net balance. A PENDAPATAN account with `credit = 1,000,000` and `debit = 200,000` (a partial reversal) would display `1,000,000` instead of the correct `800,000`.

#### Bug 4 — Net Laba/Rugi computed from corrupted totals (line 371)

```jsx
{(totalPendapatan - totalBiaya) < 0
  ? '(' + new Intl.NumberFormat().format(Math.abs(totalPendapatan - totalBiaya)) + ')'
  : new Intl.NumberFormat().format(totalPendapatan - totalBiaya)
}
```

The parenthesis-notation for negative laba is good practice. However, both `totalPendapatan` and `totalBiaya` are sourced from the one-sided sums above, so the displayed Laba/Rugi figure is wrong regardless of the formatting.

---

### `BalanceSheet.jsx`

#### Bug 5 — `totalHartaTidakLancar` sums raw `.debit` only (line 200)

```js
settotalHartaTidakLancar(
  response.data.map(harta => (harta.debit)).reduce((partialSum, a) => partialSum + a, 0)
)
```

No `parseInt`, no subtraction of credit. Only raw `.debit` is summed for fixed-asset totals. This inflates `totalHartaTidakLancar` whenever credit activity exists on fixed-asset accounts (depreciation journal entries, disposals, write-downs). Compare with `getHartaLancar` on line 146 which correctly does `parseInt(harta.debit) - parseInt(harta.credit)`.

#### Bug 6 — Dead code: `getTotalBiaya` and `getTotalPendapatan` are never called (lines 153–175)

```js
// These functions are defined but getData() never calls them:
const getTotalBiaya = async () => { ... }
const getTotalPendapatan = async () => { ... }

// getData() only calls:
const getData = () => {
  getHartaLancar()
  getHartaTidakLancar()
  getUtang()
  getModal()
  getLaba()       // ← laba comes from backend endpoint, not these functions
}
```

Additionally, both dead functions send `filters.keyword` as a param but no date filters — meaning even if they were called, they would return unfiltered totals regardless of the selected date range.

#### Bug 7 — `laba` from backend is structurally wrong (line 183–189)

```js
const getLaba = async () => {
  const params = { start_date: ..., end_date: ... }
  const response = await axios.get(endpoint.report.laba, { params })
  setlaba(parseInt(response.data))
}
```

The `/report/laba` endpoint correctly forwards date parameters to `getTotalPendapatan` and `getTotalBiaya` on the backend. Date filtering works. However, the underlying calculation in both those methods is one-sided (Finding 2), so the laba value embedded in the Balance Sheet's Ekuitas section is wrong for the same reasons as Finding 2.

---

## 9. Why Balanced Clients Do Not Disprove These Findings

Several client deployments show a balanced Balance Sheet and correct-looking Income Statement. This is expected behavior from the bugs described above, and does not indicate that those clients' code paths are different.

### The Latent Bug Principle

The one-sided aggregation bugs (Findings 2 and 5) are **data-dependent**. They only produce incorrect output when correction or reversal journal entries exist on the affected account types.

For a PENDAPATAN (Revenue) account with no reversals:
- Every entry is: `Debit Kas → Credit Pendapatan`
- The PENDAPATAN COA only ever receives Credit postings
- Therefore `sum(debit) = 0` on that COA
- `sum(credit) - sum(debit)` = `sum(credit) - 0` = `sum(credit)`

The buggy formula (only `sum(credit)`) produces the same number as the correct net formula. **The bug is invisible.**

The moment a refund, reversal, or correction is posted:
- `Debit Pendapatan → Credit Kas`
- Now `sum(debit) > 0` on the PENDAPATAN COA
- Buggy formula still returns `sum(credit)` (gross)
- Correct formula returns `sum(credit) - sum(debit)` (net)
- A gap opens equal to the total of all correction debits

The same logic applies symmetrically to BIAYA accounts — the bug surfaces when vendor credit notes, expense reversals, or any credit correction is posted.

### What Distinguishes the Broken Client

The client showing the ~2.24B discrepancy likely has:

1. **COA misclassification** (Finding 1) — this hits regardless of data cleanliness since it is a schema/metadata problem, not a transaction volume problem.
2. **A meaningful volume of correction entries** — refunds, manual journal corrections, or reversal transactions that exposed the latent aggregation bugs.
3. **More severe journals/transactions desync** — higher transaction volume combined with the race condition means more missing journal rows, widening the gap between the `transactions`-based and `journals`-based views.

### The Time Bomb Pattern

Balanced clients are effectively running with latent defects masked by clean data. As they accumulate corrections, chargebacks, write-offs, and adjustments over time, the discrepancy will emerge on those clients as well. The bugs are not isolated to one client — they are universal to the codebase.

---

## 10. Audit Tool Flaw — RecalculateLedger Command

### Classification

**Severity:** Medium  
**File:** `app/Console/Commands/RecalculateLedger.php`

### Description

The `audit:recalculate-ledger` artisan command was introduced to diagnose balance issues. However, the command itself contains an accounting logic error that makes it unsuitable for auditing liability, equity, and revenue accounts.

```php
$trueBalance = DB::table('journals')
    ->where('coa_id', $account->id)
    ->selectRaw('COALESCE(SUM(debit) - SUM(credit), 0) as balance')
    ->value('balance');
```

`SUM(debit) - SUM(credit)` is the correct formula only for **Assets and Expenses**. For Liabilities, Equity, and Revenue accounts — which have a normal Credit balance — the correct formula is `SUM(credit) - SUM(debit)`. Running this command against a PENDAPATAN or UTANG account will produce a negative "true balance" that looks like an error when it actually reflects a correctly-credited account.

This means the audit tool will:
- Report negative balances for healthy revenue accounts
- Report negative balances for healthy liability accounts
- Potentially mislead any investigation that relies on its output

The command also provides no grouping by account type (`coas.group`), no comparison against expected normal balance, and no summary of which accounts are "wrong" vs "expected negative."

---

## 11. Full Bug Summary Table

| # | Severity | Layer | File / Location | Description |
|---|---|---|---|---|
| 1 | Critical | DB (live data) | `coas` table | Kas accounts misclassified as `UTANG`, renders as negative liabilities |
| 2 | Critical | Backend | `FinanceController.php:295` | `getTotalBiaya` sums only `debit`, drops all expense credits/refunds |
| 3 | Critical | Backend | `FinanceController.php:313` | `getTotalPendapatan` sums only `credit`, drops all revenue reversals |
| 4 | Critical | Backend | `FinanceController.php:331` | `getLaba` inherits both errors — doubly inflated profit |
| 5 | High | Backend | `FinanceController.php:366` | `getOperationalIncome` reads from `transactions`, not `journals` |
| 6 | High | Backend | `FinanceController.php:379` | `getOperationalExpense` reads from `transactions`, not `journals` |
| 7 | High | Backend | All grouped report methods | Raw `debit`/`credit` columns exposed — net calculation offloaded to client |
| 8 | Critical | Frontend | `IncomeStatement.jsx:116` | Total Pendapatan only reads `.credit` — mirrors backend bug independently |
| 9 | Critical | Frontend | `IncomeStatement.jsx:132` | Total Biaya only reads `.debit` — mirrors backend bug independently |
| 10 | Major | Frontend | `IncomeStatement.jsx:308,345` | Per-row display also shows one raw column only, not net balance |
| 11 | Major | Frontend | `BalanceSheet.jsx:200` | `totalHartaTidakLancar` sums raw `.debit` only — no net, no parseInt |
| 12 | Minor | Frontend | `BalanceSheet.jsx:153–175` | `getTotalBiaya`/`getTotalPendapatan` defined but never called — dead code |
| 13 | Critical | Frontend | `BalanceSheet.jsx:183` | `laba` fetched from backend endpoint — inherits Finding 4 backend errors |
| 14 | Medium | Backend | `RecalculateLedger.php:27` | Audit command applies `debit - credit` universally — wrong for liability/revenue COAs |

---

## 12. Recommended Fix Order

Fixes should be applied in this order to produce the fastest visible improvement on the most critical discrepancy:

### Step 1 — Fix COA Classification in the Database (Finding 1)
Run a targeted SQL query to identify and correct misclassified Kas accounts. This is the only finding that produces wrong numbers regardless of data cleanliness, and it is the fastest fix since it requires no code deployment — only a data correction.

```sql
-- Identify candidates first
SELECT id, name, code, `group`
FROM coas
WHERE name LIKE '%Kas%' AND `group` = 'UTANG';

-- Then correct them
UPDATE coas
SET `group` = 'HARTA'
WHERE name LIKE '%Kas%' AND `group` = 'UTANG';
```

Verify against the known IDs (120, 123, 124, 125, 126, 273, 274, 275) as well.

### Step 2 — Fix Backend Aggregation Methods (Findings 2, 3, 4)
Refactor `getTotalPendapatan` and `getTotalBiaya` to use true net balance formulas. This also fixes `getLaba` transitively. Deploy as a single backend update.

### Step 3 — Fix Frontend Income Statement Math (Findings 8, 9, 10)
Update `IncomeStatement.jsx` to compute net values from the API response using the correct formula per account type. This is independent of the backend fix — both need to be done.

### Step 4 — Fix Frontend Balance Sheet (Finding 11)
Correct `totalHartaTidakLancar` calculation in `BalanceSheet.jsx` to match the correct `parseInt(harta.debit) - parseInt(harta.credit)` pattern used by `getHartaLancar`.

### Step 5 — Unify Operational Data Source (Findings 5, 6)
Refactor `getOperationalIncome` and `getOperationalExpense` to read from the `journals` table grouped by `coa_id` and `coas.group`, rather than from `transactions` filtered by `transaction_type_value`. This eliminates the split-brain problem.

### Step 6 — Move Net Calculation to the Backend (Finding 7)
Refactor all grouped report endpoints to return a single `balance` column per COA (computed in SQL) rather than raw `debit`/`credit` pairs. This simplifies the frontend contract and prevents future calculation errors in new views.

### Step 7 — Fix the Audit Tool (Finding 14)
Update `RecalculateLedger.php` to apply account-type-aware balance formulas using a `JOIN` on `coas.group`.

### Step 8 — Data Reconciliation
After all code fixes are deployed, run a full reconciliation to identify transactions in the `transactions` table that have no corresponding journal entries. These represent data lost to the historical race condition and may require manual journal entries to bring the ledger into balance.

---

*End of Report*
