backbone-accounting 0.12.0

GL, journals, ledger, reconciliation (extracted from bersihir)
Documentation
# Architecture

> Reader: **maintainer** · Mode: **explanation** (C4-style, top-down).

`backbone-accounting` is a **library crate** (`[lib]` only, no `main.rs`) — it is *not* itself deployable. It owns the accounting bounded context and is linked into a composing `backend-service`, which runs the Axum HTTP server, opens the PostgreSQL pool, and mounts the module's routes. Everything below describes the crate as it is consumed inside such a service.

The point: accounting is a **general-ledger engine**. Producer modules throw immutable financial facts (`AccountingPost`) at one endpoint; the module validates, journalizes, writes an append-only ledger with running balances, and emits events. It imports **no producer module** and never calls back into one. For the *why* behind this stance, see [./philosophy.md](./philosophy.md) and [ADR-001](./adr/ADR-001-gl-core-boundary.md). Terms are defined in [./glossary.md](./glossary.md).

---

## 1. Context (C4 L1)

```mermaid
graph TB
    subgraph Actors
        BK[Bookkeeper]
        AC[Accountant]
        CT[Controller]
    end

    subgraph Producers["Producer modules (upstream)"]
        BILL[billing]
        PAY[payments]
        BANK[banking]
        INV[inventory]
        AST[assets]
    end

    ACCT{{backbone-accounting<br/>GL engine}}

    subgraph Referenced["Referenced systems (logical FK only)"]
        CORP[corporate<br/>Company / Branch / Dept]
        PARTY[party<br/>Party]
        PROJ[projects<br/>Project]
        TAX[backbone-tax-id<br/>computes PPN / PPh]
    end

    SAP[sapiens<br/>User — real DB FK]

    BK --> ACCT
    AC --> ACCT
    CT --> ACCT

    BILL -- AccountingPost --> ACCT
    PAY -- AccountingPost --> ACCT
    BANK -- AccountingPost --> ACCT
    INV -- AccountingPost --> ACCT
    AST -- AccountingPost --> ACCT

    TAX -. tax lines inside AccountingPost .-> ACCT

    ACCT -. company_id/branch_id/dept_id .-> CORP
    ACCT -. party_type/party_id .-> PARTY
    ACCT -. project_id .-> PROJ
    ACCT -- audit actor FK --> SAP
```

*Notice:* every arrow into accounting is one-directional. Producers push `AccountingPost`; accounting never imports or calls them (no synchronous callback). All of `corporate`, `party`, `projects` are **logical FKs** — no DB constraint, resolved by ID only. `sapiens.User` is the **single real DB FK import**, used for audit actors. `backbone-tax-id` does not integrate directly: it computes PPN/PPh upstream and delivers tax lines *inside* the `AccountingPost` payload.

---

## 2. Containers (C4 L2)

```mermaid
graph TB
    subgraph Service["Composing backend-service"]
        AXUM[Axum HTTP server]
        subgraph Crate["backbone-accounting crate (linked in)"]
            ROUTES["AccountingModule.routes()<br/>12 CRUD / entity<br/>+ POST /accounting/posts"]
            GUARD["guarded_routes:<br/>posted GL mounted READ-ONLY"]
        end
    end

    PG[(PostgreSQL<br/>schema-per-tenant<br/>via search_path)]
    BUS[[backbone-messaging<br/>event bus]]

    AXUM --> ROUTES
    ROUTES --> GUARD
    ROUTES -->|SQLx 0.8| PG
    ROUTES -->|AccountingPostPosted / Failed| BUS
```

*Notice:* the crate is a passenger — the `backend-service` owns the process, the pool, and the port. The module contributes a `Router`. Two write surfaces exist: the standard **12 Backbone CRUD endpoints per entity** (at `/api/v1/{collection}`, via `BackboneCrudHandler`) and the one non-CRUD **`POST /accounting/posts`** — the entire system's GL write path. `create_guarded_accounting_routes(&module)` remounts the four posted GL entities (Journal, JournalLine, Ledger, AccountingPost) **read-only**: their only sanctioned writer is the posting path. Master/config entities keep full CRUD. Persistence is **schema-per-tenant** — isolation is a `search_path` concern, not a `provider_id` column (see [ADR-001 #4](./adr/ADR-001-gl-core-boundary.md)). Events are published to `backbone-messaging`.

---

## 3. Components / modules (C4 L3)

The crate follows the standard DDD 4-layer shape. `lib.rs` holds the `AccountingModule` and its builder.

| Layer | Path | Responsibility |
|-------|------|----------------|
| Domain | `src/domain/{entity,repositories}` | Entity structs; repository ports. |
| Application | `src/application/{service,dto,workflows}` | Service type aliases; DTOs; posting workflow. |
| Infrastructure | `src/infrastructure/{persistence,cache,messaging,jobs}` | Repository newtypes; caching; event publishing; background jobs. |
| Presentation | `src/presentation/{http,dto,middleware,grpc}` | Handlers; wire DTOs; middleware; gRPC (**present but disabled**). |
| Composition | `src/routes/`, `src/seeders/` | Route composers (stateless + stateful); test-data seeders. |

**Services** are type aliases to `GenericCrudService`; **repositories** are thin newtypes over `GenericCrudRepository`. The `AccountingModule` builder wires all **10 services**, e.g. `AccountRepository::new(pool.clone())` → `AccountService::with_repository(repo)`, inside `// <<< CUSTOM` / `// END CUSTOM` markers reserved for custom wiring.

```mermaid
graph LR
    B["AccountingModule::builder()<br/>.with_database(pool).build()?"]
    B --> S1[AccountService]
    B --> S2[AccountingPostService]
    B --> S3[CostCenterService]
    B --> S4[FinancialStatementService]
    B --> S5[FiscalPeriodService]
    B --> S6[JournalService]
    B --> S7[JournalLineService]
    B --> S8[LedgerService]
    B --> S9[ReconciliationService]
    B --> S10[ReconciliationItemService]
    S1 -.type alias.-> G[GenericCrudService]
    S6 -.type alias.-> G
    S8 -.type alias.-> G
    R[Repository newtypes] -.wrap.-> GR[GenericCrudRepository]
    G --> R
```

*Notice:* there is no hand-rolled service `impl` and no ad-hoc repository — everything routes through `GenericCrudService` / `GenericCrudRepository`. The 10 entities are: **Account, AccountingPost, CostCenter, FinancialStatement, FiscalPeriod, Journal, JournalLine, Ledger, Reconciliation, ReconciliationItem**.

**gRPC is present-but-disabled.** `tonic 0.12` and `prost` are declared in `Cargo.toml`, but `index.model.yaml` disables the `graphql`, `grpc`, and `proto` generators — so no gRPC/proto code is currently generated.

**Cascade-child exception:** `JournalLine` and `ReconciliationItem` carry no `@audit_metadata`, so their `soft_delete` / `restore` / `empty_trash` / `list_deleted` endpoints are **not** generated — they are managed through their parent aggregate.

For how to extend safely, see [./extension-guide.md](./extension-guide.md) and [./developer-guide.md](./developer-guide.md).

---

## 4. Data & control flow — posting an `AccountingPost`

The posting path (`POST /accounting/posts`) is the only sanctioned GL writer. It runs the FSD §4 hooks (`validate_posting`, `assemble_journal`, `write_ledger`, `update_account_balance`, `emit_post_event`) in sequence.

```mermaid
sequenceDiagram
    participant P as Producer module
    participant EP as POST /accounting/posts
    participant J as Journal + JournalLines
    participant L as Ledger (append-only)
    participant A as Account
    participant BUS as backbone-messaging

    P->>EP: AccountingPost (posting_status=pending)
    EP->>EP: validate R1-R6 (status -> processing)
    alt validation passes
        EP->>J: assemble Journal + N JournalLine
        loop for each line
            EP->>L: read last sequence_number -> balance_before
            EP->>L: write immutable row (balance_after, balance_change)
        end
        EP->>A: update current_balance
        EP->>EP: set journal_id, posting_status=posted
        EP->>BUS: emit AccountingPostPosted
    else validation fails
        EP->>EP: posting_status=failed (no ledger rows)
        EP->>BUS: emit AccountingPostFailed
        Note over EP: retry while retry_count < max_retries
    end
```

*Notice:* the ledger is **append-only** (R8) and each row computes its running balance from the previous `sequence_number` (`balance_before`), then sets `balance_after` / `balance_change` (R9). On the **failure path** no ledger rows are written — `posting_status=failed`, `AccountingPostFailed` is emitted, and the post is retried while `retry_count < max_retries`. There is no synchronous callback into the producer; the only signals back are the two Tier-A events: `AccountingPostPosted { post_id, source_type, source_id, journal_id, status }` and `AccountingPostFailed { post_id, source_type, source_id, error_code, error_message }`.

**Idempotency** is enforced at the DB layer: a partial unique index on `(company_id, source_type, source_id, posting_type) WHERE posting_status='posted'` blocks double-posting the same source fact ([ADR-002](./adr/ADR-002-ledger-write-path-integrity.md)).

**Reversal, briefly:** posted entries are never edited (R7). A reversal produces a **mirror journal** (via the `link_reversal` hook), which lands in the current open period — the `block_closed_period` hook prevents writes into closed periods.

Rules R1–R11 and golden cases G1–G8 live in [./brd.md](./brd.md); the posting flow and hook contracts are detailed in [./fsd.md](./fsd.md).

---

## 5. Where to change what

See [./maintainer-guide.md](./maintainer-guide.md) for procedures; this table points you at the right layer.

| To change… | Go to |
|------------|-------|
| An entity's fields, columns, or CRUD surface | schema YAML (SSoT) → regenerate; see [./maintainer-guide.md](./maintainer-guide.md) |
| Posting validation, journalizing, ledger, balance, events | hooks in `schema/hooks/` (`validate_posting`, `assemble_journal`, `write_ledger`, `update_account_balance`, `emit_post_event`, `link_reversal`, `block_closed_period`) + `application/workflows` |
| Custom business logic on a service | `*_service_custom.rs` or `// <<< CUSTOM` markers |
| Service wiring / new entity registration | `AccountingModule` builder in `lib.rs` (CUSTOM markers) |
| Which GL entities are read-only vs full CRUD | `src/presentation/http/guarded_routes.rs` |
| Event publishing / bus integration | `src/infrastructure/messaging` |
| Tenant isolation / `search_path` behavior | [ADR-001](./adr/ADR-001-gl-core-boundary.md); persistence layer |
| Idempotency / ledger write-path integrity | migrations (partial unique index) + [ADR-002](./adr/ADR-002-ledger-write-path-integrity.md) |
| Adding a non-CRUD endpoint | handler in `presentation/http/` + composer in `routes/`; see [./extension-guide.md](./extension-guide.md) |