Repository navigation
Releases: munashe-chivandire/zimra-fdms
Releases · munashe-chivandire/zimra-fdms
Release list
v0.4.0
The core now runs anywhere. This is the first step of the 1.0 plan: no
fiscal behaviour changes, and 0.3.x code keeps working.
Added
zimra-fdms/core: the fiscal engine with no platform imports. No
node:*, noBuffer, nofetch, nostructuredClone. SHA-256, MD5 and
base64 are implemented in plain TypeScript, so hashing stays synchronous
and works on Hermes, Deno, Bun and browsers without polyfills. A lint step
(npm run lint:core) fails the build if a platform reference creeps in,
and a test drives a full fiscal day throughdist/coreinside a VM
context that has none of those globals.Signerinterface.FiscalDevicetakes aSignerand never sees key
material, so the key can live in a PEM, Android Keystore, an HSM or a
cloud KMS.PemSignerin the Node package wraps the 0.3.x PEM and imports
the key once instead of once per receipt.- CSR built in the core.
buildCsr(signer, commonName)writes the
PKCS#10 request with a small DER encoder and has theSignersign it, so a
key that cannot be exported can still register. Output is byte-identical
to the @peculiar/x509 request 0.3.x sent, minus the randomised signature. Transportinterface.NodeTransportuses one keep-alivehttps.Agent
per device so the TLS handshake and PEM parse happen once per process.
FetchTransportcovers the Public endpoints on any runtime withfetch.ServerCorrectedClock.FiscalDevicelearns the offset to FDMS from
every responseDateheader and stamps receipts and day-open times with
server time.device.clock.offsetMsand.confidenceare readable.renewCertificate()in the core reuses the device's key. The Node
FiscalDevicekeeps the 0.3.x rotate-the-key behaviour under
renewWithNewKey().@zimra-fdms/react-nativeinpackages/react-native:KeystoreSigner
andOkHttpTransportover a Kotlin module. The keystore and mutual-TLS
logic ran on an Android 16 emulator throughandroid/check, a plain-Java
twin built with the SDK tools, driven byscripts/android-check.sh: key
in Android Keystore, CSR signed by it, RegisterDevice, mTLS, receipts,
reconcile, CloseDay. That run showed a keystore key needsDIGEST_NONE
for TLS client auth; the Kotlin module sets it. The Kotlin has not yet
been built with Gradle.- Integer cents.
ReceiptInputamounts are a whole number of major
units orcents("11.50"); a fractional JS number throws, naming the
field. Tax, totals, payments and counters are computed in integer cents
and rounded per line.receiptInputFromJson()converts amounts that came
through JSON with up to two places; the CLI and MCP server apply it. - Crash safety. With a
Storage,FiscalDevicewrites a
pending-submit marker before every SubmitReceipt andreconcile()settles
it on the next start: confirms with GetStatus, resubmits the identical
signed receipt, or reports a rejection.submitReceipt(),openDay()
andcloseDay()throwPendingSubmitErrorwhile a marker exists. The
highest global number ever issued is kept in storage andopenDay()
takes the largest of server, stored and given values.FileStorageon
Node writes atomically and uses the CLI profile file names, so the CLI
and MCP server get this for free (stateDir, and they reconcile on every
command). - Append-only journal.
OfflineReceiptQueuesigns at sale time
(signReceipt()+applyPrepared()), appends to aJournal, and
commits a cursor per delivered receipt.FileJournal(JSONL) on Node,
MemoryJournalin core,journalFromQueueStorage()wraps a 0.3.x
QueueStorageand upgrades its unsigned entries on load.
oldestPendingAgeMsfor the 72-hour window. A flush that died after
writing the marker is settled throughreconcile(), not sent twice. zimra-fdms/simulatorandnpx zimra-fdms-simulator: a local FDMS
with a generated CA and real mutual TLS. Issues certificates from the
SDK's CSR, verifies every signature with the shared canonical-string
code, replays RCPT010/011/012/013/014/020/021/030/031, the asynchronous
close withBadCertificateSignatureandReceiptsWithValidationErrors,
and the GetStatus under-report. Faults: dropped connections, 5xx, delay,
skewedDate, lost SubmitReceipt answer.baseUrlon the client options
andZIMRA_BASE_URL/ZIMRA_CAon the CLI, MCP server and e2e script
point at it.- Conformance vectors in
vectors/zimra-fdms-vectors.json(also
zimra-fdms/vectors) for a fixed test key, andnpx zimra-fdms-conformance -- <command>to check any implementation against them over a stdin/stdout
JSON protocol.src/conformance/reference.tsis the responder to port. - Error catalogue.
ERROR_CATALOGUE,explainCode(),
FdmsApiError.explain()and.supportCode,explainValidationError().
Colour, cause, fix and whether the day is still closable per code, with
observed entries marked. Fixes the old RCPT012 hint, which described a
duplicate invoice number; the live meaning is a non-sequential global
number. - Idempotent calls (GET endpoints and Ping) retry twice with jittered
backoff on a network failure (retriesin the client options).
SubmitReceipt, OpenDay and CloseDay are never retried by the client. MIGRATION.mdwalks through every 0.3.x change.- CI runs on Node 18, 20, 22 and 24, lints the core for platform imports,
runs the conformance runner against the reference, and typechecks the
Android bindings against real React Native types.
Changed
- Sources moved to
src/coreandsrc/node.import ... from "zimra-fdms"
still resolves to the Node bundle; deep imports ofsrc/*.tspaths change. p1363ToDertakes and returnsUint8Array. ABufferis aUint8Array,
so callers passing one are unaffected; callers doing
.toString("base64")on the result should usetoBase64()from the core.signCanonicalString(signer, canonical, format)takes aSignerinstead
of a PEM string. Wrap the PEM:new PemSigner(privateKeyPem).registerDevice(device, key, signer, options)is the core signature. The
Node export keepsregisterDevice(device, key, options)generating an
exportable PEM as before.package-lock.jsonregenerated against registry.npmjs.org; 0.3.1's
lockfile resolved every package to a mirror.- TypeScript target is ES2020 so the compiled core runs on Hermes.
closeDay()no longer clears stored day state on an accepted close; the
close is asynchronous and a rejected one needs the counters for a retry.
clearPersistedState()deletes it once GetStatus reports
FiscalDayClosed.READMEdependency count corrected: three runtime dependencies, none in
the core.
Upgrading from 0.3.x: see MIGRATION.md.
0.3.1
Recovery fixes. Everything here came out of one stuck fiscal day.
Fixed
- Day-close recovery signed with today's date. CloseDay is signed over the
date the day was opened. FDMS never reports that date, and the recovery
path (no localday-state.json) assumed today, so a day opened on an
earlier date failed withBadCertificateSignature.zimra-fdms day close
takes--date YYYY-MM-DD; the MCPclose_fiscal_daytool takes
fiscalDayDate; both say when they are assuming today. - Local day state deleted before the close was confirmed. CloseDay is
asynchronous. The CLI and MCP tool removedday-state.jsonas soon as the
request was accepted, so a close that FDMS later rejected lost the real
counters and date. State is now cleared only once FDMS reports
FiscalDayClosed. - Receipt numbering after a future-dated receipt.
GetStatus.lastReceiptGlobalNo
is the number of the receipt with the latestreceiptDate, not the highest
number issued. After a future-dated receipt (Yellow RCPT031) it under-reports,
and the next day's first receipt got a Red RCPT012. The profile now keeps the
highest number it has issued inlast-receipt-global-no.jsonandopenDay
uses the larger of that and the server's value. Library users can pass
openDay(undefined, new Date(), { lastReceiptGlobalNo }). ValidationError.validationErrorTextrenamed tovalidationErrorDescription
to match what FDMS actually sends.zimra-fdms submitand the MCPsubmit_receipttool acceptreceiptDate
(ISO 8601) for backfilling.
Added
- Red validation errors are tracked and block the close. A day holding a
receipt with a Red validation error (RCPT012, RCPT030, …) cannot be closed
by the device; FDMS rejects it withReceiptsWithValidationErrorsand only
ZIMRA can close it.submitReceiptrecords Red errors in the day state,
submitwarns immediately, andday close/close_fiscal_dayrefuse up
front with the portal link instead of failing 30 seconds later.--force/
force: truesubmits anyway. Library:DayNotClosableError,
FiscalDayState.redErrors. scripts/provoke-rcpt030.tsreproduces the RCPT031 → RCPT030 sequence.
Observed FDMS behaviour (test environment, 2026-08-23)
| Code | Colour | Day still closable by device |
|---|---|---|
| RCPT031 future receipt date | Yellow | yes |
| RCPT014 receipt date before day opened | Yellow | yes |
| RCPT030 receipt date earlier than previous receipt | Red | no |
| RCPT012 global number not sequential | Red | no |
After a future-dated receipt, every later receipt must carry a date strictly
after it (the SDK nudges auto-generated dates by one second) until real time
catches up. Submitting a receipt with the real time before then is RCPT030
and bricks the day.