type
Plugin
title
nx-register
description
The register/accounting plugin: posts money, voids it without deleting it, locks periods, reconciles drawer counts.
tags
plugin, accounting, ledger, sqlite, register
status
stable
stale_after
Fri Apr 30 2027 02:00:00 GMT+0200 (Central European Summer Time)
generated
day-to-day-task/deepseek-v4.1-flash · Fri Oct 30 2026 12:25:00 GMT+0100 (Central European Standard Time)
verified
process:live-acceptance-run · Fri Oct 30 2026 12:25:00 GMT+0100 (Central European Standard Time)
sources
repo → https://github.com/nextrum-sy/nx-register; host → https://github.com/nextrum-sy/simple-lms

nx-register — the register plugin

The plugin id is re, so every table it owns starts with re_. It posts money, voids it without deleting it, locks periods and reconciles drawer counts. The host sees none of that — only Post, Void, Close, Count and the balances.

flowchart TD subgraph host["Host"] boot["boot<br/>Register → Migrate (hard) → InitAll (soft)"] tab["settings tab<br/>plugin-re"] save["save<br/>Put → Update → audit_log<br/>one transaction"] enabled["enabled = registered AND initialised<br/>a failed Init → no tab"] ttl["peers: 5s settings TTL re-read"] end subgraph plugin["nx-register"] set["Settings struct<br/>LockEnforced bool (critical)<br/>VarianceCategory string"] val["validate<br/>amounts in minor units · dates required<br/>ref pair atomic · meta ≤ 4096"] cat["MethodCatalog<br/>host tags + requires_reference"] svc["Service<br/>Post → 1 movement ⇒ 1 line<br/>Void → status + reversal line<br/>Close / Count → lock, variance"] st["Store<br/>Filter (+Status, dates, meta) · Page"] end subgraph schema["Schema"] mov[("re_movements<br/>document: date · type · currency · method<br/>ref_type · ref_id · status · meta")] lin[("re_lines<br/>the money: amount_minor · side")] box[("re_boxes · re_closes · re_counts")] setrows[("re_settings · re_migrations")] inv["invariant<br/>Sums(lines) == Σ movements where status=posted"] end boot --> svc tab -->|"Fields() + GetAll()"| set save -->|"Put / Update"| set set --> setrows set --> cfg["Config() → service.Config"] svc --> val svc --> cat svc --> st st --> mov st --> lin st --> box mov --> inv lin --> inv enabled --> tab ttl --> set class boot,tab,save,enabled,ttl host class set,val,cat,svc,st plugin class mov,lin,box,setrows,inv store class cfg host classDef host fill:#15314a,stroke:#2f5d84,color:#e6edf7 classDef plugin fill:#163a2f,stroke:#2f7d5b,color:#e6edf7 classDef store fill:#3a2f16,stroke:#8a7327,color:#f2e8cf

The money rules

rule why
one movement posts exactly one line a partial posting is not a thing that can exist; the pair is the unit
a void flips the status and appends a reversal line nothing is ever deleted, so the history stays auditable and the balance nets to zero
every query sums status = posted only the naive sum over all movements overstates by exactly the voided amount
a movement carries a required date an undated row silently lands outside every period report
(ref_type, ref_id) is atomic and indexed a host joins its own documents on the pair; half a reference is a bug
amount_minor is an integer no float ever touches money
a box's currency and method are frozen once it has lines re-denominating a box rewrites history instead of correcting it

Settings it actually has

type Settings struct {
    LockEnforced     bool   // refuse a posting dated inside a locked period
    VarianceCategory string // category used for a drawer-count difference
}

Two fields, and both are load-bearing: the first gates writes, the second decides how a cash-count difference is booked. No method list — that vocabulary belongs to the host, which passes its own tags to Service(ctx, tags); the plugin's DefaultMethodCatalog is only the fallback when a host passes none.

What it refuses

Wiring traps worth remembering

Status — plugin migration complete

Done. simple-lms runs only nx-register; the pilot package go-register is gone from go.mod, and its tables (movements, lines, boxes, cash_boxes, drawer_closes, drawer_counts, register_schema_migrations) no longer exist in a database created by the squashed baseline — only the plugin's re_* tables do.

Verified end to end on the deployed tenant (test-lms, image v20261030-71c4137) through the browser, not through unit tests:

flow result
student fee payment IN-00001, 75,000 SYP cash → re_movements receipt 7,500,000 minor, ref_type=payment, ref_id = payment id, meta carries payment_id/kind/student_id/student_fee_id
expense OUT-00002, 5,000 SYP → expense movement, line out 500,000
salary payment OUT-00003, 20,193 SYP → salary movement, line out 2,019,300
box balance 75,000 − 5,000 − 20,193 = 49,807 SYP, matching Σ in − Σ out exactly
receipt rendered with number, date, student, grade/class, amount, method, currency

The host never touched a re_ table in any of those flows: each one went through the money service into the plugin.

UI gaps — all five closed and re-verified

The first cashier run exposed five gaps; they were fixed in 9526ff5 and every one was re-checked in the browser on the rebuilt tenant (v20261030-e8eb493):

gap fix how it was verified
pay form pre-filled 0, so a ticked fee paid nothing amount inputs pre-fill the fee's remaining balance opened S-0425's pay screen: inputs read 10000 / 8000 / 12000, one submit → receipt IN-00001
/ledger defaulted to one box, so other currencies looked empty no box_id → all boxes, per-currency totals /ledger with no query string listed 4 movements with a box column
salary payment gave no confirmation flash carrying the payment number toast read "Salary paid · OUT-00003"
first expense was OUT-00002 (shared sequence) per-prefix sequences from MAX(number) first expense recorded as OUT-00001
movement detail page unrouted GET /ledger/movements/:id clicked a ledger row → fields + lines table rendered

Acceptance run on the rebuilt tenant

Fresh database from the squashed baseline (001_schema.sql, 002_seed_settings.sql), roles seeded from Go at boot (role-manager, role-accountant, role-teacher, role-parent), business data copied back with users remapped by role code (manager 1, accountant 1, teacher 20, parent 42, students 503, enrollments 550).

step result
login as admin dashboard renders
/roles 4 built-in roles, catalogue matrix = 22 resources × actions
pay fee (75,000 SYP, ref INV-9001) receipt IN-00001, reference printed
expense (5,000 SYP) OUT-00001
salary (20,193 SYP) OUT-00002 + toast
cancel a salary payment OUT-00003 → cancelled, movement voided, reversal line appended
ledger 5 rows including the reversal, box column populated
balance SYP cash 49,807 = posted-only sum, exactly

The invariant holds after a void: Sums(lines) == Σ movements where status=posted.

Contract status: core flag

nx-register implements plugin.Core() from nx-common v0.3.0 (APIVersion 3) and currently returns true, so a failed Register/Migrate/Init refuses to start the host rather than hiding the money features.

That is a deliberate interim setting: the owner's position is that the register is not core — a school must keep running students, attendance and fees when the ledger is unavailable. Flipping it is one line (return false) plus the host's already-tested degrade path; it is deferred rather than decided against.

Rendered from plugins/re/register-plugin.md · OKF bundle · regenerate with npm run render