backbone-pos
The retail counter path of an Indonesia-first ERP's Financials pillar. A Backbone domain module
(a [lib]-only crate, 4-layer DDD, schema-YAML as the source of truth) that owns the cashier session,
the sale ticket, tender, and the drawer.
It posts no general ledger itself. On recognition it orchestrates two downstream emitters through
outbound ports — backbone-billing (raise + post the real Sales Invoice → revenue) and
backbone-payment (settle the tender) — so retail reuses the same GL posts as web/B2B sales. A cash
sale books Dr A/R · Cr Revenue (billing) then Dr Cash · Cr A/R (payment); A/R nets to zero at the
counter.
Entities
Defined in schema/models/ — the single source of truth, in the pos Postgres schema:
| Entity | Table | Role |
|---|---|---|
| PosProfile | pos_profiles |
Register configuration: outlet, currency (IDR), and the GL account refs the handoff needs. |
| PosOpeningEntry | pos_opening_entries |
A cashier session (till open) with opening float per method. Sales ring against an open session. |
| PosInvoice (+ PosInvoiceItem) | pos_invoices / pos_invoice_items |
The ticket: server-side money, IDR receipt rounding, billing_invoice_id, returns. |
| PosPayment | pos_payments |
A tender line (cash / card / QRIS / e-wallet / bank transfer / virtual account). |
| PosClosingEntry | pos_closing_entries |
The Z-report: per-method expected-vs-counted drawer reconciliation. |
Cross-module ids (accounting, billing, payment, party, catalog, organization, sapiens) are logical foreign keys — no DB constraint — so the modules stay independently deployable.
Quickstart
Requires Rust 2021, the metaphor CLI (0.2.0+) on PATH, and a reachable PostgreSQL.
The
backbone-*framework crates are git dependencies pinned tobranch = "main"(seeCargo.toml), so the crate builds anywhere on disk with no path fix-up. For a release, pin them to a tag or commit (tag = "vX.Y.Z"/rev = "<sha>") for reproducibility.
Mounting the module
backbone-pos is a library — a backend-service composes it and mounts its router. Prefer the
guarded surface (read documents + validated writes; generic CRUD mutation is not mounted):
let pos = builder.with_database.build?;
let router = create_guarded_pos_routes;
Validated write routes: POST /pos-sessions (open), /pos-sales (ring), /pos-tenders (add tender),
/pos-sessions/close. Sale recognition and returns drive billing + payment through the
BillingPort/PaymentPort, so they are service/job-driven, not HTTP routes — see
tests/retail_sale_seam.rs for a working composition.
PosModule::all_crud_routes() exposes the full unguarded CRUD surface for trusted/admin/seeding only.
Documentation
Start with the handbook — philosophy, background, technology, architecture, maintainer guide, developer guide, contribution guide, glossary, and ADRs, each written for a named reader.
- Developer Guide — install → quickstart → walk a counter sale.
- Architecture — the C4 view and the retail-sale seam traced end-to-end.
- Maintainer Guide — schema-YAML SSoT, regeneration, the hand-authored write path + ports.
- Product docs · Business flows · ADR-001 (POS boundary + retail seam).
The single source of truth
schema/models/*.model.yaml defines every entity. The codegen pipeline produces the entity struct,
DTOs, migration, repository, service, generated CRUD handler, and routes. Regeneration preserves only
code inside // <<< CUSTOM … // END CUSTOM markers, *_custom.rs files, and user_owned paths (in
metaphor.codegen.yaml) — which protect the hand-authored heart:
pos_write_service.rs (the validated write path + retail orchestrator), pos_ports.rs
(BillingPort/PaymentPort), pos_events.rs (the event union + sink), and guarded_routes.rs.
Edit the schema first; never hand-edit generated code outside a protected region. See the Maintainer Guide for the full workflow.