jPOS Project Blog https://jpos.org/blog jPOS Project Blog 2026年6月30日 13:30:00 GMT https://validator.w3.org/feed/docs/rss2.html https://github.com/jpmonette/feed en <![cdata[jpos Client Simulator: functional testing on the Control Plane]]> https://jpos.org/blog/clientsim-demo https://jpos.org/blog/clientsim-demo 2026年6月30日 13:30:00 GMT Payment systems are only as trustworthy as the testing behind them.

For an issuer, correctness is not just a response code. It is the ISO 8583 message on the wire, the product rules that shaped the decision, the ledger entries that were posted, and the evidence an operator can inspect later.

The new Client Simulator demo shows that loop end to end on the jPOS Control Plane: a live jCard issuer, a real CMF channel, a functional test suite, raw logs, and a reconciled general ledger.

What the demo shows

The video follows a complete functional test flow for a live jCard issuer:

  • the jPOS Control Plane running in Kubernetes, with the jCard issuer deployed as a separate pod
  • the ACME entity, its GOLD card product, BIN, authorization fee, velocity profile, cardholders, and active cards
  • a Client Simulator project with endpoint, packager, suite, and cases bundled together
  • a live CMF channel to the jcard-acme issuer pod
  • a CMF v3 ISO 8583 packager describing the wire format
  • the jcard-auth suite: 39 cases and 133 steps
  • cases that combine platform actions with ISO requests
  • field rules, assertion rules, and response grading
  • a full suite run against the live pod
  • raw ISO request and response traffic in the Log Viewer
  • the resulting ledger entries on the cardholder account
  • AI-assisted suite authoring and the path from correctness testing to load and soak testing

The important part is that these are not separate demos. The same run connects product configuration, generated traffic, operational logs, and accounting state.

More than message blasting

A simple ISO message blaster can tell you whether a host answered. It cannot tell you whether the test was staged correctly, whether the response matched the business rule, or whether the financial entries landed where they should.

The Client Simulator is a functional test harness. A case is made of typed steps:

  • platform actions can reset velocity counters, fund a cardholder, or assert a ledger entry
  • ISO request steps build real messages and send them to the issuer
  • assertions grade the response field by field
  • variables and helpers make each run deterministic without hard-coding every value

That means a single case can prepare its own state, drive wire traffic, inspect the issuer response, and verify the books.

Evidence on the wire

Every ISO step is built from explicit rules. Some fields are fixed literals, some come from helpers such as trace-number and timestamp generators, some come from the case variables, and others can be produced by small sandboxed scripts.

The response is graded the same way. In the demo, an authorization step expects response code 0000 and checks for the balance information in data element 54. The run captures the expected and received values, so the result is not just "green"; it is inspectable.

Because the test drives the live issuer, the traffic is also visible in the Log Viewer. The operator can inspect the actual request and response messages, such as 2100/2110 and 2200/2210, exactly as they crossed the channel.

That is the useful evidence trail: the test intent, the generated ISO message, the issuer response, and the structured log record all line up.

The ledger is part of the test

For card issuing, a passed response is not enough. The issuer also has to post the right entries.

The demo follows account 2.10.ACME.0000001, the cardholder account used during the run. With the relevant layers selected, the balance history shows settled and pending activity. The account statement shows every transaction touched by the suite.

Opening one authorization shows the general ledger legs behind it:

  • the cardholder is debited on the pending layer for the hold
  • the acquirer is credited for the purchase amount
  • the issuer fee is booked to its own revenue account
  • debits and credits balance

The simulator mocked none of this. It drove the real issuer, and the issuer posted to the real ledger.

AI authoring and scale

The demo closes with where this is going next. The Control Plane already has preconfigured suites for jCard and jPTS, and the integrated assistant can help author or edit them.

The important detail is the review boundary. A prompt can ask for a larger fixture set, a mixed traffic stream, or a fan-out from one known-good authorization into thousands of variations. The assistant returns a reviewable change set. The operator still decides what becomes part of the suite and what runs.

That keeps the correctness workflow controlled while making the harness easier to expand.

Once the suite exists, the same idea scales beyond functional correctness. The Client Simulator runs inside the Control Plane, on Kubernetes. The same structured cases that prove behavior on one issuer pod can be scaled horizontally to drive larger traffic patterns for load, endurance, and soak testing.

Why this matters

Payment testing needs to connect intent with evidence. A useful test has to show what it meant to do, what it sent, what came back, and what financial state changed as a result.

That is the practical value of putting the Client Simulator inside the jPOS Control Plane. It uses live endpoints, real packagers, structured assertions, operational logs, and the same ledger the issuer writes to in normal operation.

The result is a testing surface that is not just faster to run, but easier to trust.

]]>
mgl clientsim testing iso8583 jcard jpos
<![cdata[jpos Log Viewer, with chat in the loop]]> https://jpos.org/blog/logview-chat-demo https://jpos.org/blog/logview-chat-demo 2026年6月24日 15:45:00 GMT Operational AI is useful only if it brings you closer to the evidence.

For logs, that evidence is not a paragraph of generated text. It is the indexed event, the timestamp, the realm, the host, the trace identifier, the original structured payload, and the surrounding events that explain what happened before and after.

That is the design point of the latest Log Viewer demo. Chat is now part of the operator workflow, but it is not a replacement for the Log Viewer. It is a faster way to ask the first question, keep context, and move toward the same structured evidence an operator would inspect manually.

What the demo shows

The video shows the jPOS Control Plane with the Log Viewer and the assistant working together:

  • the operator starts from an operational question instead of a hand-written query
  • the assistant keeps the conversation tied to the current user and entity context
  • tool activity remains visible as part of the chat flow
  • the UI can move from a chat answer to the relevant application page
  • the Log Viewer still provides the searchable, faceted, inspectable event record
  • event details remain available in human-readable and JSON forms

The important part is the handoff. Chat can help get from intent to context, but the system still lands on inspectable state.

Chat is not the audit trail

It is tempting to think of an assistant as the new operational console: ask a question, get an answer, move on.

That is not good enough for regulated systems. If an incident matters, the answer has to be grounded in something durable and inspectable. A generated summary can be helpful, but it cannot be the record.

The Log Viewer already gives MGL a structured log surface: event kind, realm, host, trace identifier, payload excerpt, full JSON source, time histogram, column facets, and trace linking. The assistant adds a conversational entry point on top of that, not a second source of truth beside it.

This is the right split:

  • chat handles intent, context, and explanation
  • tools retrieve or prepare system state
  • the UI shows the resulting page or evidence
  • logs remain structured events, not prose

That keeps the operator in control. The assistant can shorten the path, but it does not hide the path.

Same permissions, same context

The chat service runs inside the same Control Plane session model as the rest of MGL. The WebSocket connection is authenticated from the user's session cookie, the active entity is validated, and permissions are resolved for that entity before tools execute.

That matters because AI features often fail by becoming a side door. If the assistant can see or do things the user cannot, it becomes a privilege-escalation surface. If it ignores tenant or entity boundaries, it becomes a data-leak surface.

MGL's chat flow is deliberately boring in this respect: the assistant receives the user's current entity context, tool calls are permission-gated, and failed authorization is not something the model is allowed to work around.

For an operator, that means the chat is another way into the system, not another security model.

From question to page

One practical detail in the demo is that tool results can carry navigation hints. When the assistant uses a tool, the chat layer can point the UI to the related page: a chart account, an account statement, a transaction, a report, or a posting draft.

That pattern is important for logs too. An operational answer should not end at "I found something." It should lead to the place where the operator can inspect, filter, compare, and copy the evidence.

The Log Viewer remains the right place for that work:

  • use the histogram to zoom into the incident window
  • filter by kind, realm, and host
  • follow trace identifiers across related events
  • open one or more events in detail
  • copy the original JSON when an incident report needs exact source data

Chat makes the first step more natural. The viewer makes the conclusion verifiable.

Why this matters

The useful version of AI in operations is not a chatbot that talks about the system from the outside. It is an authenticated client of the system, using the same tools, the same permissions, and the same structured data as the rest of the application.

That is the direction MGL keeps moving toward: operational workflows where AI can help, but where every important result remains tied to durable, inspectable application state.

For Log Viewer, that means the assistant can help an operator ask better questions and get to the right place faster. The evidence still lives where it should: in the structured log index, visible through the same UI an operator can trust without AI.

]]>
mgl logging ai observability jpos
<![cdata[log Ingestor: one writer, many readers]]> https://jpos.org/blog/log-ingestor-architecture https://jpos.org/blog/log-ingestor-architecture 2026年6月14日 19:00:00 GMT Operational logs are part of the control surface. In a single-node demo it is tempting to let the same process receive logs, write the search index, and serve the viewer. That works until the Control Plane itself needs to scale, restart independently, or run without holding the only Lucene write lock.

jPOS now splits that responsibility into a dedicated log ingestion path: workloads emit structured jPOS JSONL logs to standard output, Fluent Bit transports labelled pod logs, a log-ingestor service owns the write side, and the jPOS Control Plane reads the shared index read-only.

What the demo shows

The video starts with the architecture and then switches to a live Log Viewer tour:

  • workload pods and Control Plane pods emit structured JSONL log events
  • pods opt into collection with the controlplane.jpos.org/log-collect=true label
  • Fluent Bit tails container stdout and forwards matching events over HTTP
  • the log-ingestor receives batches on /admin/logs/ingest
  • the ingestor is the only component that writes the Lucene index and BinLog payload store
  • the shared volume holds both /data/log-index and /data/log-payload
  • Control Plane replicas mount the same volume read-only and serve the Log Viewer from it
  • the live viewer filters by realm, searches the structured payload, opens event details, and shows events from several hosts converging into one index

The important part is not just that logs show up in a UI. The important part is the ownership model.

One writer

Lucene indexes have a clear rule: there should be one writer. The new log-ingestor service makes that rule explicit.

In Kubernetes, the ingestor runs as a single replica with a Recreate deployment strategy. It owns the index writer and the BinLog payload writer. Fluent Bit posts events to the ingestor's HTTP endpoint, and the ingestor appends them to the shared storage.

That keeps write locking simple. There is no race between Control Plane replicas, no accidental second index writer, and no need for every UI pod to perform ingestion work.

Many readers

The Control Plane now has a separate read-only backend for the Log Viewer. LogReaderService opens the shared Lucene index and BinLog payload store without taking the writer role, refreshes its reader periodically, and lets the existing Log Viewer handlers resolve that read backend.

That means Control Plane pods can be scaled horizontally. Each replica can serve the same operational view, but none of them owns the write path. If one Control Plane pod restarts, ingestion continues. If the Control Plane scales from one replica to three, the log index still has one writer.

This is the same shape we want elsewhere in regulated infrastructure: separate the mutation path from the observation path, make the owner of each side obvious, and keep the operational contract easy to reason about.

Fluent Bit as transport

Fluent Bit is deliberately used as transport, not as the source of truth.

The DaemonSet tails /var/log/containers, applies Kubernetes metadata, filters for the jPOS log-collection label, and sends matching records to the ingestor. It does not parse business meaning into a separate observability schema, and it does not own the index.

The structure still comes from jPOS log events. That is why the viewer can facet by kind, realm, host, and trace identifier, and why opening an event can show the original structured payload instead of a guessed text parse.

Shared storage

The ingestor writes the Lucene index and the BinLog payload store to a persistent volume. The Control Plane mounts the same claim read-only.

For production, the chart is designed around a real ReadWriteMany storage class such as NFS, CephFS, EFS, or Longhorn RWX. For local single-node development, the chart can run on local-path style storage with pod affinity so the writer and readers land where the shared path is available.

The storage contract is intentionally boring: one component writes; many components read.

Ready for tenant isolation

The chart already carries the next shape: one ingestor and one isolated storage volume per tenant.

Today, an empty tenant list renders the system-wide ingestor. When tenant-specific deployments are enabled, the same template structure can render a separate ingestor, service, and persistent claim per tenant. That keeps the scaling model aligned with the data-isolation model instead of turning logs into a shared cross-tenant bucket.

Why this matters

The Log Viewer started as a useful way to search structured jPOS events. The log ingestor turns it into a deployable architecture for clustered systems.

It gives jPOS a clean pipeline:

  • applications emit structured events
  • Fluent Bit transports selected pod stdout
  • one ingestor writes the durable index and payload store
  • Control Plane replicas read that data read-only
  • operators get the same faceted, searchable view without coupling UI scale to ingestion

That is the point of the new architecture: not more moving parts for their own sake, but a smaller responsibility for each moving part.

]]>
logging observability kubernetes jpos
<![cdata[latest and Greatest]]> https://jpos.org/blog/latest-and-greatest https://jpos.org/blog/latest-and-greatest 2026年5月12日 12:00:00 GMT The best jPOS version is usually the latest one.

That may sound counterintuitive if your organization treats -SNAPSHOT as a synonym for "unstable", but that is not how we operate jPOS and jPOS-EE. We work hard to keep the latest snapshots production-ready. We run them in production ourselves. They are where the current fixes, optimizations, dependency updates, security improvements, and newly certified behavior land first.

In practice, the latest snapshot is often the most advanced, optimized, and feature-rich jPOS version available.

Why latest matters

jPOS is used in real payment systems, by real institutions, under real certification pressure. That gives us a continuous stream of feedback:

  • network certification findings
  • edge cases in ISO 8583 behavior
  • performance improvements
  • dependency refreshes
  • security scanner results
  • operational hardening
  • compatibility issues found in customer environments

When those improvements are made, they go into the active development line first.

That is why staying close to the latest jPOS and jPOS-EE snapshots is usually the healthiest way to develop. You get the benefit of the current work instead of discovering months later that your project has drifted away from the version the rest of the ecosystem is already validating.

The official source for these artifacts is the jPOS Maven repository:

https://jpos.org/maven

That is the repository we publish to, and that is the one customers should use when consuming jPOS and jPOS-EE snapshots.

Production-ready does not mean magically risk-free

There is an important distinction here.

We strive to keep the latest snapshots production-ready, and we run them that way. But a snapshot is still a moving artifact. For short periods of time, a particular snapshot build may contain a problem. When that happens, we treat it with urgency and fix it with the highest priority.

That is engineering reality. It is also one of the reasons staying in touch during development matters.

If you develop against the current snapshot and report issues when you find them, we can usually respond quickly, while the change is still fresh and before your project has accumulated a large version gap.

If you freeze development for a long time on an older build, then later jump forward just before production, you may face a much larger set of accumulated changes at once. That is avoidable technical debt.

Development versus production

Our recommendation is simple:

  • Use the latest -SNAPSHOT during development.
  • Pin an exact timestamped snapshot when moving to production.

During development, depending on:

implementation "org.jpos:jpos:3.0.2-SNAPSHOT"

keeps you on the moving development line. Your builds will pick up the current snapshot published under that version.

That is what you want while actively developing, testing, and certifying. It keeps your project in the loop.

For production, however, you usually want repeatability. The artifact deployed today should be the same artifact deployed tomorrow, unless you intentionally change it. That means pinning the exact timestamped Maven snapshot build.

A pinned dependency looks like this:

implementation "org.jpos:jpos:3.0.2-20260509.153915-15"

That version is no longer a moving target. It identifies one concrete build.

How Maven snapshot pinning works

Maven snapshots are published with metadata. For example, the metadata for the current 3.0.2-SNAPSHOT jPOS line is available at:

https://jpos.org/maven/org/jpos/jpos/3.0.2-SNAPSHOT/maven-metadata.xml

Inside that file you will find entries like:

<snapshot>
<timestamp>20260509.153915</timestamp>
<buildNumber>15</buildNumber>
</snapshot>

and, more explicitly:

<snapshotVersion>
<extension>jar</extension>
<value>3.0.2-20260509.153915-15</value>
<updated>20260509153915</updated>
</snapshotVersion>

The value element is the exact version you can pin in your build.

So this development dependency:

implementation "org.jpos:jpos:3.0.2-SNAPSHOT"

can become this production dependency:

implementation "org.jpos:jpos:3.0.2-20260509.153915-15"

The same idea for jPOS-EE

jPOS-EE artifacts are published the same way. For example, the jposee-dbsupport metadata for the current 3.0.2-SNAPSHOT line is available at:

https://jpos.org/maven/org/jpos/ee/jposee-dbsupport/3.0.2-SNAPSHOT/maven-metadata.xml

That metadata contains a timestamped artifact value such as:

<snapshotVersion>
<extension>jar</extension>
<value>3.0.2-20260511.202033-12</value>
<updated>20260511202033</updated>
</snapshotVersion>

So this development dependency:

implementation "org.jpos.ee:jposee-dbsupport:3.0.2-SNAPSHOT"

can become this production dependency:

implementation "org.jpos.ee:jposee-dbsupport:3.0.2-20260511.202033-12"

The same approach applies to other jPOS and jPOS-EE artifacts. Use the Maven metadata for the snapshot line you are consuming, find the timestamped artifact value, and pin that value for production.

A practical workflow

A good workflow looks like this:

  1. Develop against the latest snapshot.
  2. Run your automated tests continuously.
  3. Stay in contact with the jPOS team if you hit an issue.
  4. Let us fix problems while you are still in the development cycle.
  5. When you are ready for production, inspect Maven metadata.
  6. Pin the exact timestamped snapshot that passed your QA process.
  7. Promote that pinned build through your production release process.

This gives you both sides of the tradeoff:

  • fast access to the latest fixes and improvements during development
  • reproducible, auditable builds in production

That is much better than freezing too early, drifting for months, and then having to absorb a large update under release pressure.

Supply-chain hygiene

We are also fully aware of modern supply-chain expectations, including ISO/IEC 20243-style concerns around secure engineering, provenance, and dependency management.

jPOS and jPOS-EE are not just source repositories with occasional releases. They are maintained production components. We keep dependencies current, respond to reported issues, and produce appropriate SBOMs so customers can integrate jPOS artifacts into their own governance, scanning, and audit processes.

Dependency updates are not cosmetic. In many cases, they are the reason the latest snapshot is the right place to be: it includes the current security posture of the project, not the posture from the last final release.

The rule of thumb

Use -SNAPSHOT while you are building.

Pin the timestamped snapshot when you are deploying.

That is the balance we recommend: stay close to the latest and greatest during development, avoid accumulating technical debt, report issues early, and then lock down the exact artifact that your QA process approved for production.

The latest jPOS snapshot is not a random nightly build. It is the active, production-oriented development line of the project.

Treat it accordingly.

]]>
jpos jpos-ee maven snapshots sbom supply-chain
<![cdata[extending jPOS structured audit logs]]> https://jpos.org/blog/extending-jpos-structured-audit-logs https://jpos.org/blog/extending-jpos-structured-audit-logs 2026年5月06日 12:00:00 GMT jPOS 3 introduced structured audit logging as a first-class feature: instead of writing only text lines, a log event can carry typed payloads such as start, stop, deploy, connect, disconnect, txn, and so on.

That structure is what makes tools such as the jPOS Log Viewer possible. The viewer can filter, facet, correlate, and render events because it is not guessing meaning from text. It is reading fields.

Until now, however, those typed audit events were effectively limited to the event classes shipped inside jPOS itself. That was fine for core runtime events, but it was not enough for real applications.

A jPOS-EE module, an application module, or a customer-specific extension may have its own operational event worth logging in a structured way:

  • an HTTP access event from QRest,
  • an authentication event from a web application,
  • a business workflow transition,
  • a settlement file import,
  • a reconciliation result,
  • a domain-specific warning that deserves first-class fields.

Those should not have to be flattened into strings. They should be allowed to live next to the built-in jPOS audit events.

That is now possible.

The problem

Structured audit log events are serialized polymorphically. A typical payload contains a short type discriminator named t:

{
"t":"warn",
"warn":"disk space is low"
}

or:

{
"t":"txn",
"name":"authorization",
"id":123456
}

The t value is intentionally stable and compact. It lets a reader—human or machine—know what shape the rest of the object has.

Previously, the list of known subtypes was declared directly on AuditLogEvent using Jackson annotations. That meant adding a new event type required changing jPOS itself.

That does not scale. jPOS-EE and application modules need to define their own events without sending every type back to the jPOS core repository.

The new SPI

AuditLogEvent is still the marker interface for typed structured log payloads:

packageorg.jpos.log;

publicinterfaceAuditLogEvent{}

The difference is that the mapping between a stable type id and its Java class now lives in a registry.

External modules contribute mappings by implementing:

packageorg.jpos.log;

publicinterfaceAuditLogEventProvider{
Collection<AuditLogEventType>types();
}

Each mapping is represented by:

packageorg.jpos.log;

publicrecordAuditLogEventType(
String name,
Class<?extendsAuditLogEvent> clazz
){}

The registry loads built-in jPOS event types first, then discovers external providers using Java's ServiceLoader:

AuditLogEventRegistry.register(objectMapper);

jPOS' JSON and XML log renderers already call this registry, so modules usually only need to provide the event class and the provider.

Built-in type ids remain unchanged:

warn, start, stop, deploy, undeploy, msg, shutdown,
deploy-activity, throwable, license, sysinfo,
connect, disconnect, listen, session-start, session-end, txn

External providers cannot shadow those names. If a provider tries to register a conflicting type id, startup fails fast instead of silently producing ambiguous logs.

A small example

Suppose an application wants to log a structured event every time it imports a settlement file.

First, define the event:

packagecom.acme.settlement;

importorg.jpos.log.AuditLogEvent;

publicrecordSettlementImport(
String file,
int records,
int accepted,
int rejected,
long durationMs
)implementsAuditLogEvent{}

Pick a stable type id. Keep it short, descriptive, and unlikely to collide with another module. A project prefix is a good habit:

acme-settlement-import

Then provide the mapping:

packagecom.acme.settlement;

importorg.jpos.log.AuditLogEventProvider;
importorg.jpos.log.AuditLogEventType;

importjava.util.Collection;
importjava.util.List;

publicclassSettlementAuditLogEventProviderimplementsAuditLogEventProvider{
@Override
publicCollection<AuditLogEventType>types(){
returnList.of(
newAuditLogEventType(
"acme-settlement-import",
SettlementImport.class
)
);
}
}

Finally, register the provider using Java's standard service-provider mechanism. Add this file to the module JAR:

META-INF/services/org.jpos.log.AuditLogEventProvider

with one line:

com.acme.settlement.SettlementAuditLogEventProvider

Now the event can be added to a regular jPOS LogEvent payload:

importorg.jpos.util.LogEvent;
importorg.jpos.util.Logger;

LogEvent evt =getLog().createInfo();
evt.addMessage(newSettlementImport(
"settlement-2026年05月06日.csv",
1280,
1274,
6,
842
));
Logger.log(evt);

When written through the structured JSON log writer, the payload remains typed:

{
"ts":"2026-05-06T15:00:00Z",
"kind":"info",
"tags":{
"realm":"settlement.import"
},
"payload":[
{
"t":"acme-settlement-import",
"file":"settlement-2026年05月06日.csv",
"records":1280,
"accepted":1274,
"rejected":6,
"durationMs":842
}
]
}

That is the important part: no fragile parsing, no regular expressions, no guessing that the third token in a line is a status. The event has fields.

Why this matters

The immediate use case is QRest.

Today QRest can be noisy at connection boundaries: accepted connection, closed connection, timeout, and so on. That is useful at times, but it is not the same as an access log.

What we really want from an HTTP server is a single event per request, similar in spirit to what Apache or NGINX access logs provide, but structured from the start:

{
"t":"http-access",
"method":"POST",
"path":"/api/cards",
"status":201,
"bytes":348,
"durationMs":37,
"remoteAddr":"203.0.113.10",
"userAgent":"curl/8.7.1"
}

Once QRest emits that as an AuditLogEvent, the same structured log pipeline can ingest it. The Log Viewer can filter by status code, path, method, duration, or remote address. Operators can find slow requests, failed requests, noisy clients, and deployment regressions without scraping text.

This also creates a pattern for other jPOS-EE modules and application modules. They can publish domain-specific operational events while remaining compatible with the same structured log tools.

A few guidelines

When defining your own audit events:

  • Use records where possible. They are compact and serialize naturally.
  • Keep fields flat unless nesting adds real value.
  • Prefer stable names over clever names.
  • Keep type ids short, lowercase, and namespaced when appropriate.
  • Avoid secrets, PANs, tokens, passwords, authorization headers, and raw request bodies.
  • Log identifiers that help correlation, not sensitive payloads.
  • Treat the event shape as a public contract once logs are consumed by tools.

Structured logging is only useful if the structure is boring and predictable.

Where this goes next

The SPI is the foundation. It lets jPOS remain small while allowing the ecosystem around it to grow structured events independently.

The next natural step is to use it in jPOS-EE, starting with QRest access events. After that, other modules can follow: authentication, sysconfig changes, crypto-service operations, scheduler activity, settlement workflows, and application-specific events.

The payoff is visible in the jPOS Log Viewer demo: once logs are structured at the source, operational tools no longer need to reverse-engineer text. They can index, filter, render, and correlate the data directly.

That was always the point of jPOS logging. This SPI simply opens that model to the rest of the application stack.

]]>
jpos logging structured-audit-log spi
<![cdata[jpos/mgl Kubernetes Deployments]]> https://jpos.org/blog/jpos-mgl-kubernetes-deployments https://jpos.org/blog/jpos-mgl-kubernetes-deployments 2026年5月03日 10:00:00 GMT Deploying financial infrastructure should not depend on someone remembering the right kubectl context, pasting the right kubeconfig into the right terminal, or manually reconstructing which Helm values were used last time. The deployment path is part of the control surface. It needs the same auditability, separation, and repeatability as the ledger itself.

The jPOS Control Plane brings Kubernetes deployment into the operator console. It stores target cluster credentials encrypted at rest, registers Helm charts from OCI registries, turns JSON Schema-backed chart values into typed forms, binds everything into reusable release plans, and drives dry-run, preflight, apply, resources, and logs from one audited UI.

What the demo shows

The video walks through the full deployment workflow from a fresh jPOS/MGL instance:

  • bootstrap the deployment encryption keyring using an RSA-4096 OpenPGP keypair
  • run an encrypt/decrypt round-trip test to confirm the crypto layer is live
  • export a passphrase-protected keyring backup and verify it without exposing key material
  • register a Kubernetes target cluster using in-cluster ServiceAccount credentials
  • constrain that target with a namespace allowlist
  • register a Helm chart from an OCI registry
  • create a values profile using a schema-driven form instead of raw YAML
  • create a release plan that binds chart, target, namespace, values profile, and Helm release name
  • run a required dry-run before the first real apply
  • execute RBAC and reachability preflight checks
  • apply the release using Helm's atomic upgrade/install path
  • inspect created resources and Helm history
  • stream pod logs through the same encrypted target credentials
  • return to the dashboard and see the system-status banner report deployment health

The demo deploys mgl-iso-sim, a jPOS-based ISO 8583 autoresponder, into an iso-sim namespace. The important part is not the sample workload; it is the deployment contract around it.

Encrypted target credentials

Kubernetes credentials are powerful. If an application stores them, it has to treat them as production secrets.

The jPOS Control Plane starts by bootstrapping an encryption keyring. The private half of the keyring is protected by an unlock passphrase, and the keyring itself is stored durably so it can survive redeployments. Cluster credentials are encrypted at rest under this keyring.

The demo also shows backup and verification. A .kbk backup file captures the keyring and wrapped data keys in a passphrase-protected envelope. Verification decrypts the backup and displays metadata—fingerprint, capture timestamp, matching-keyring status—without exposing key bytes.

This matters operationally. Losing the keyring means losing access to encrypted deployment credentials. Backups are not an afterthought; they are part of the deployment lifecycle.

Target clusters with blast-radius limits

A target cluster defines where the control plane is allowed to deploy. In the demo, jPOS/MGL is running inside the same Kubernetes cluster, so it can use in-cluster credentials: the pod's ServiceAccount token is read at operation time and used to construct a kubeconfig on the fly.

The target also carries a namespace allowlist. That allowlist is the first guardrail. A release plan cannot casually point at an arbitrary namespace; it must stay inside the cluster and namespace boundaries defined for that target.

This is deliberately narrower than "give the UI a kubeconfig and hope operators are careful." The target definition is an authorization boundary.

Helm charts as catalog entries

The chart catalog stores immutable references to Helm charts in OCI registries. Registering a chart pulls its metadata, records its version, and captures the chart-layer digest.

The demo registers mgl-iso-sim from an in-cluster OCI registry. Once registered, the chart becomes a selectable deployment artifact. Operators do not need to remember a registry URL, chart version, or digest each time they deploy. They choose from the catalog.

Charts that ship a JSON Schema get a typed values form. Instead of editing YAML by hand, operators see fields with appropriate input types, defaults, and validation. Raw YAML remains available as an escape hatch, but the default path is structured.

Release plans

A release plan ties everything together:

  • chart
  • target cluster
  • values profile
  • namespace
  • Helm release name
  • plan status

Plans are reusable. Applying a plan creates a release record, but the plan remains as the stable definition of what should be deployed and where.

This is useful for audit and operations. "What did we deploy?" is not hidden in shell history. "Which values were used?" is not a pasted YAML fragment in a ticket. The plan is explicit, stored, and reviewable.

Dry-run first, then preflight

The jPOS Control Plane requires a successful dry-run before the first real apply on a release plan. Dry-run renders the chart and asks the Kubernetes API server to validate the manifests without creating resources.

After dry-run succeeds, the preflight panel becomes available. Preflight checks cluster reachability and namespace-scoped RBAC before Helm is allowed to perform the real operation. If the credentials cannot create or update the resources Helm needs, the operator learns that before the apply starts.

This sequence catches the common deployment failures early:

  • malformed or incompatible values
  • invalid rendered manifests
  • missing namespace permissions
  • unreachable cluster
  • insufficient RBAC verbs

The goal is not to make deployment magical. The goal is to make failure visible before it becomes a half-deployed production incident.

Atomic apply and live visibility

The apply path uses Helm upgrade/install with atomic behavior. If the release cannot come up cleanly, Helm rolls it back instead of leaving a partial state behind.

Once the release succeeds, the control plane shows the resources owned by the release and the Helm history behind it. The pod logs panel reads logs through the same encrypted target credentials. Operators can inspect current or previous container instances, choose tail size, and filter client-side while troubleshooting.

That last part is important: deployment does not end when Helm exits. Operators need immediate visibility into what was created and whether it is alive.

The design choice

The jPOS Control Plane is not trying to replace Kubernetes, Helm, or GitOps. Those tools remain the substrate. What it adds is an operator-facing control plane around a specific class of deployments: regulated systems where credentials, approvals, repeatability, and audit trail matter.

A terminal can deploy a chart. A CI job can deploy a chart. But neither automatically gives a back-office operator a safe, constrained, auditable workflow with encrypted cluster credentials, schema-driven values, required dry-run, RBAC preflight, atomic apply, and live logs in one place.

That is the point of the deployment plugin: make deployment an application workflow, not a shell ritual.

]]>
jpos mgl kubernetes helm devops
<![cdata[cmf v3 and the ISO 8583 Dataset Model]]> https://jpos.org/blog/cmfv3-datasets https://jpos.org/blog/cmfv3-datasets 2026年4月05日 10:00:00 GMT The jPOS Common Message Format specification is getting an update. CMF v3 is still a work in progress, but an early-access draft is available at jpos.org/doc/jPOS-CMFv3.pdf for anyone who wants to follow along. The most significant addition in v3 is first-class support for the ISO 8583 dataset model—and that's what this post is about.

What datasets are

ISO 8583 has long defined certain fields as composite containers—variable-length binary fields that carry structured sub-fields rather than a single atomic value. DE-55 (ICC data) is the most widely known: it holds raw BER-TLV data from the EMV specifications. Fields like DE-34 (electronic commerce data), DE-43 (card acceptor), and DE-49 (verification data) carry structured content using a similar pattern, formalized under the dataset model.

A dataset is a self-describing envelope inside a composite field. Each dataset has:

  • an identifier (one byte, 0x010xFE) that says what kind of data it contains
  • a length (2 bytes, big-endian) indicating the size of its content
  • a payload encoded according to the dataset's format

Multiple datasets can appear in sequence inside a single field. DE-49, for example, can carry a dataset 0x01 for TLV-encoded currency data and a dataset 0x71 for bitmap-structured verification data, back to back in the same wire bytes.

Two encoding formats: TLV and DBM

ISO 8583 defines two encoding formats for dataset payloads.

TLV (Tag-Length-Value) is BER-TLV encoding, the same format used by EMV and ISO/IEC 7816. Tags are 1, 2, or 3 bytes: a leading byte whose low 5 bits are all set (0x1F) signals that more tag bytes follow, with bit 7 (the high bit) of each subsequent byte acting as the continuation flag. Lengths use BER definite form: values up to 127 fit in a single byte; for longer values, the first byte has bit 7 set and the lower 7 bits indicate how many additional bytes encode the actual length (so a 300-byte value needs two additional length bytes after the indicator byte). Values are raw bytes. DE-34 dataset 0x01 (authentication request data) and DE-55 (ICC data) both use TLV.

DBM (Dataset Bitmap) carries structured fields using an ISO 8583-style bitmap. The payload starts with a 2-byte bitmap whose bits announce the presence of the corresponding elements; element values follow in order. Bit 1 of each word is a continuation bit—if set, the next byte extends the bitmap. DBM datasets can also carry trailing TLV elements after the bitmap section for extended or proprietary data.

DE-34 datasets 0x730x77 (authentication response data) and DE-49 dataset 0x71 (verification data) use DBM. DE-55 uses TLV throughout.

What it looks like in jPOS

CMF v3 support in jPOS introduces ISODatasetField, ISODataset, and DatasetPackager. The ISOMsg API extends to dataset paths using dot notation: "field.datasetId.elementId". The same path syntax works for both with() and get().

Building a message

ISOMsg msg =newISOMsg("0100");
msg.setPackager(newGenericPackager("jar:packager/cmfv3.xml"));

msg.with("55.0x9F26",ISOUtil.hex2byte("1122334455667788"))// ICC: TLV tag 9F26
.with("55.0x9F10",ISOUtil.hex2byte("06011203A0B800"))// ICC: TLV tag 9F10
.with("55.0x95",ISOUtil.hex2byte("0000000000"))// ICC: TLV tag 95
.with("49.0x71.1","1")// Verification: DBM dataset 0x71, element 1
.with("49.0x71.2","1234");// Verification: DBM dataset 0x71, element 2

The path "55.0x9F26" addresses field 55, TLV element with tag 0x9F26. The path "49.0x71.2" addresses field 49, dataset 0x71, DBM element at bit position 2.

Reading a message

getString() and getBytes() accept the same paths:

ISOMsg unpacked =newISOMsg();
unpacked.setPackager(packager);
unpacked.unpack(packed);

// TLV elements from DE-55
byte[] cryptogram = unpacked.getBytes("55.0x9F26");
byte[] iad = unpacked.getBytes("55.0x9F10");

// DBM elements from DE-49 dataset 0x71
String flag = unpacked.getString("49.0x71.1");
String amount = unpacked.getString("49.0x71.2");

No casting, no intermediate objects. The path resolves through the field, the dataset, and the element in one call.

Mixed datasets in a single field

A single field can hold multiple datasets, mixing TLV and DBM in the same wire encoding:

msg.with("49.0x01.0x5F2A",ISOUtil.hex2byte("0840"))// TLV dataset 0x01: transaction currency code
.with("49.0x71.1","1")// DBM dataset 0x71: element 1
.with("49.0x71.2","USD");// DBM dataset 0x71: element 2

Reading back:

byte[] currency = unpacked.getBytes("49.0x01.0x5F2A");
String element1 = unpacked.getString("49.0x71.1");
String element2 = unpacked.getString("49.0x71.2");

Backward compatibility with DE-55

One design goal of CMF v3 is that the wire bytes for DE-55 are identical between the legacy packager (cmf.xml) and the new dataset packager (cmfv3.xml). The ICC data that was previously an opaque ISOBinaryField containing raw BER-TLV is now an ISODatasetField containing a TLV ISODataset—but the bytes on the wire are unchanged. Existing integrations that write DE-55 with the old packager can be read by a system using the new one without protocol negotiation.

ISO 20022 transport

CMF v3 also defines DE-114 as the ISO 20022 transport field—a slot for carrying a complete ISO 20022 XML document (UTF-8 encoded, full Document element with namespace URI identifying the message type) alongside the ISO 8583 message. This enables jPTS to act as a bridge between ISO 8583 and ISO 20022 messaging without a separate protocol translation layer.

The CMF v3 draft covers the dataset model in detail and the DE-114 transport mechanism. It is a work in progress—sections are still being written and some field definitions are pending—but it is already usable as a reference for implementation work.

Early-access draft: jpos.org/doc/jPOS-CMFv3.pdf

]]>
jpos iso8583 cmf payments devtools
<![cdata[massivegl Posting Templates]]> https://jpos.org/blog/massivegl-posting-templates https://jpos.org/blog/massivegl-posting-templates 2026年4月02日 10:00:00 GMT Every ledger has a set of transactions it posts over and over. The accounts change, the amounts change, sometimes the counterparty changes—but the structure is always the same. A fee charge is always a debit to the customer account and a credit to fee income. A settlement is always the same four entries. A foreign exchange conversion follows the same arithmetic every time.

Freeform posting can handle all of these, but it puts the entire burden of correctness on the operator: right accounts, right sides, right layer, right formula—every time, by hand. Templates solve this. A template captures the invariant structure of a transaction and exposes only the parts that actually vary. Everything else is handled by the ledger.

What a template is

A template is a versioned posting pattern. It defines:

  • Posting lines—the entries that make up the transaction, with accounts, sides, and layers
  • Parameters—the variable inputs an operator or system provides at posting time
  • Derived values—computed from parameters using an expression language
  • Validations—rules that must pass before the transaction is accepted

When a template is used, the operator provides only the parameters. The rest is pre-wired. The ledger produces a balanced, validated transaction with a permanent link back to the template ID and version that created it.

Parameterizing accounts

The most visible use of parameters is amounts—but accounts can be parameterized too, and this is where templates become genuinely powerful.

A bill payment template might have a fixed debit to the customer's clearing account and a credit to whichever payee account the operator specifies. An accounts receivable payment template fixes the credit side to AR income and takes the debtor account as a parameter—so the same template handles every customer. A funds transfer template takes both the source and destination accounts as parameters, turning a two-sided entry into a single form with two lookup fields.

This means you can define one template for an entire class of transactions rather than one per account or counterparty. The structure is guaranteed; only the addressable parts are exposed.

The expression language

Amount modes give you four options for each posting line:

ModeWhat it does
FixedA constant hardcoded into the template
ParameterA named value the operator provides
FormulaAn expression derived from parameters, balances, or other values
BalanceThe current balance of an account—for closing or sweep entries

Formulas unlock the most interesting cases. A fee template can compute the charge automatically:

fee = round(amount * fee_rate, 2)

One posting line debits the customer account for amount + fee, another credits the service account for amount, and a third credits the fee income account for fee. The operator enters one number; the template derives the rest.

Foreign exchange works the same way. An FX conversion template takes the source amount and a rate parameter:

target_amount = round(source_amount * fx_rate, 2)

Debit the foreign currency account for source_amount, credit the local currency account for target_amount. No manual arithmetic, no rounding errors, no mismatched entries.

A revolving credit interest template can compute the monthly charge from the outstanding balance:

interest = round(outstanding * monthly_rate, 2)

Run this as a batch job at end of month across every eligible account—same template, different parameter values, thousands of balanced transactions generated without operator involvement.

Versioning and audit

Every edit to a template creates a new version. The original is never modified. Every posted transaction stores the template ID and the exact version number that produced it—FEE_CHARGE:3, FX_CONVERSION:1, REVOLVING_INTEREST:2.

If a fee rate changes, you update the template. Transactions before the change are still accurately described by the version that created them. There is no ambiguity about what formula produced a given entry, even years later.

Where templates fit in operations

Templates cover a wide range of use cases across the transaction lifecycle:

Operator-driven transactions—emergency card funding, manual adjustments, dispute credits, goodwill reversals. An operator opens the template palette (⌘K), selects the template by name, enters the variable parts, and posts. Accounts are pre-wired, amounts are validated, and the result is auditable.

Back-office workflows—fee collection, interest accrual, foreign exchange settlement, interbank reconciliation. These follow fixed patterns with computed amounts. Templates make them fast and consistent.

Batch and scheduled jobs—end-of-month interest posting, revolving credit charges, account maintenance fees, dormancy charges. A job iterates over eligible accounts, calls the template with the appropriate parameters for each, and posts. The template enforces structure; the job provides the data.

Integration points—payment processors, card networks, and external systems often need to trigger specific ledger entries. Templates give these integrations a stable, versioned contract. The external system provides parameters; the ledger handles the accounting.

Templates and dynamic rules together

Templates prevent structural errors at entry time—wrong accounts, unbalanced entries, missing fields. Dynamic rules prevent business logic violations at commit time—balance limits, date restrictions, amount caps.

They are complementary. A template guarantees the form of a transaction; a dynamic rule guarantees the outcome satisfies your business constraints. Together they give you both automation and control.

]]>
mgl accounting general-ledger automation devtools
<![cdata[massivegl Dynamic Rules]]> https://jpos.org/blog/massivegl-dynamic-rules https://jpos.org/blog/massivegl-dynamic-rules 2026年3月31日 10:00:00 GMT Most ledger enforcement is hardcoded. If the business needs a new limit—a maximum account balance, a cap on transaction size, a rule that blocks weekend postings—someone files a ticket, a developer writes a check, the code ships in the next release. That cycle takes days or weeks, and the logic lives in application code rather than in the ledger where it belongs.

MGL solves this with Dynamic Rules. A dynamic rule is a CEL expression that the system evaluates on every posting, in real time, before the transaction is committed. If the expression returns false, the transaction is denied. No code change required. No deployment.

What the demo shows

The video walks through the full dynamic rules lifecycle from scratch:

  • create an entity, journal, and three accounts (Cash, Bank, Opening Balance)
  • post opening balances to establish a starting position
  • navigate to Admin → Rules and create a max_balance rule with the expression balance_after <= 100000.00
  • watch the expression validated in real time as you type; the Save button only enables when syntax is correct
  • observe the rule in DRAFT status—it cannot be enforced until certified by a second user
  • create an auditor user, log in as them, and certify the rule; the system verifies the certifier is not the author and computes a cryptographic hash of the expression
  • log back in as admin, assign the rule at the journal level so it covers every account and every layer
  • post a transaction that stays under the limit—it clears the rule and is tagged with the rule identity
  • attempt a posting that would push Cash to 110,000—it is denied with an error showing the rule name, the current balance, the projected balance after, and the expression that failed
  • browse the built-in help documentation, available in five languages

The Clark-Wilson model

The most important design decision in dynamic rules is the separation between authoring and certification. A rule in DRAFT status is visible but inert—it cannot be assigned or enforced. Only a different user can certify it.

This is the Clark-Wilson integrity model applied to ledger rules: the person who writes a rule cannot activate it. Certifying a rule transitions it to CERTIFIED status, computes a cryptographic hash of the expression, and permanently records who approved it and when. Any subsequent tampering with the stored expression—directly in the database—is detected at evaluation time by comparing the hash, and the rule is refused.

The practical effect is that no single person can introduce a posting rule without review. This is the same control you apply to payment approvals and journal entries; it applies equally to the rules that govern them.

The expression language

Rules are written in CEL (Common Expression Language), a safe, statically-typed expression language developed at Google. CEL expressions cannot access the network, the filesystem, or any state outside the activation context—they are pure functions of their inputs.

Every expression receives these variables:

VariableTypeDescription
balancedoubleAccount balance before this entry
balance_afterdoubleProjected balance after this entry (balance + entry_amount)
entry_amountdoubleSigned impact of this entry (positive = increases balance)
entry_layerintLayer number
entry_is_debitboolTrue if debit
entry_is_creditboolTrue if credit
account_codestringAccount code
account_typestring"DEBIT" or "CREDIT"
txn_post_datestringPosting date (ISO format)
txn_detailstringTransaction memo
txn_tagsstringTransaction tags
paramstringExtra parameter from the rule assignment

The param variable is particularly useful for reuse. A single entry_amount <= double(param) rule can be assigned to different accounts with different thresholds—one assignment might carry "5000", another "50000".

Assignment scope

A certified rule does nothing until it is assigned. Assignments can be scoped at three levels:

  • Final account—applies to postings on that specific account
  • Composite account—applies to every final account under that composite, recursively
  • Journal—applies to every posting in the journal, regardless of account

Layer filtering is also available: you can restrict an assignment to specific layers, leaving others unchecked.

One rule can have multiple assignments. Assignments can be removed without modifying or re-certifying the rule.

What the error looks like

When a transaction is denied, the error message includes everything needed to understand why:

Rule 'max_balance:1' denied transaction on account 1111
balance=40000.00, balance_after=110000.00
expression: balance_after <= 100000.00

The rule code, its version, both balances, and the expression are all present. No digging through logs.

]]>
mgl accounting general-ledger controls devtools
<![cdata[massivegl Virtual Layers]]> https://jpos.org/blog/massivegl-virtual-layers https://jpos.org/blog/massivegl-virtual-layers 2026年3月30日 10:00:00 GMT Most accounting systems that want to show derived figures—variance against budget, percentage execution, a consolidated multi-currency total—end up solving the problem the same way: periodic batch jobs that write synthetic entries to hold the computed values, or reporting scripts that reconstruct the computation at query time outside the ledger.

Both approaches have the same flaw. The derived figures live in a different place than the authoritative entries. They get stale. They diverge. Reconciling them back to source is always someone's problem.

MGL solves this with virtual layers. A virtual layer carries a formula instead of entries. Its balance is computed on the fly from physical layers—always derived from the same source of truth, always accurate to the query date, with no batch job required.

What the demo shows

The video walks through virtual layer configuration and use from end to end:

  • navigate to journal administration and inspect the Layers section
  • add a Variance layer (L0 - L1), a Budget Execution layer (pct(L0, L1)), and an FX conversion layer (fx(L840, "USD/UYU"))
  • view an account's balance history and select a virtual layer — the graph plots the computed values with no stored entries behind them
  • open an account statement filtered to a virtual layer, where the entries are sourced from the referenced physical layers and an informational banner explains the distinction
  • see physical and virtual layers side by side in a trial balance report

The virtual layers appear in italic with a "(v)" suffix throughout the interface, making it clear at a glance which columns are derived and which are stored.

Physical versus virtual

A physical layer stores entries. Every debit and credit posted to that layer is recorded in the database and contributes to its running balance. Layer 0 is the default; layer 840 typically holds USD amounts; layer 1 is often used for budget.

A virtual layer holds no entries. It defines a formula, and whenever its balance is queried the engine evaluates that formula against the balances of the physical (or other virtual) layers it references. The result is accurate to any query date you provide, including historical dates.

The practical effect is that physical layers stay clean—they contain only what was actually posted. Derived figures are always recomputed from that clean source, which means they can never drift out of sync with the underlying data.

The formula language

Formulas reference layers using L followed by the layer number: L0, L1, L840. Standard arithmetic applies. A handful of built-in functions handle the cases that come up repeatedly in financial work:

ExpressionWhat it computes
L0 - L1Actuals minus budget (variance)
pct(L0, L1)Actuals as a percentage of budget
pct(L0 - L1, L1)Relative variance percentage
fx(L840, "USD/UYU")USD layer converted to Uruguayan pesos
L0 + fx(L840, "USD/UYU")Local currency plus converted foreign amount
max(L0 - L1, 0)Overspend only — variance floored at zero
if(L0 > L1, L0 - L1, 0)Conditional: only positive variance

pct() deserves a specific mention. Division in a formula (L0 / L1) will throw if the denominator is zero — which is the right behavior when a formula author explicitly divides. pct() returns zero on a zero denominator. In financial reporting, zero denominators are routine: new accounts, unfunded budget lines, inactive cost centers. A report that crashes on those cases is not usable in production.

FX conversion via fx() uses the rate for the query date, fetched from MGL's rate store. Historical balance queries use historical rates automatically—no separate configuration required.

Composability

Virtual layers can reference other virtual layers. A formula like if(L0 > 0, pct(L0, L1), 0)—show budget execution only when there are actuals—works because virtual layer resolution is recursive, with circular reference detection. If layer A references layer B and layer B references layer A, the engine throws rather than looping.

This composability means complex analytical dimensions can be built up incrementally from simpler ones, using the same formula language throughout.

Where virtual layers appear

Virtual layers participate in the same interfaces as physical layers:

  • Account balance history—select a virtual layer in the layer picker; the history graph plots computed values at each date
  • Account statements—entries are sourced from the referenced physical layers; the statement header notes the virtual layer context
  • Reports—virtual layer columns appear alongside physical ones in trial balances and balance sheets
  • API queriesGLSession.getBalance() accepts virtual layer IDs the same way it accepts physical ones; the distinction is invisible to callers

The layer administration table shows a Virtual badge on virtual layers. Elsewhere in the UI, italic text and the "(v)" suffix are the visual markers.

The design choice

The alternative to virtual layers is materialization: running a job that writes derived values into the database so they're available at query time without computation. Materialization has its place—for large aggregations where query time matters more than freshness. But for the kinds of derived dimensions that appear in ledger reporting (variance, percentages, FX consolidation), the latency budget is generous and the freshness requirement is strict. A variance figure that was correct at last night's batch run is not the same as one that reflects this morning's postings.

Virtual layers resolve on the fly. They are always fresh. The formulas are defined once, in the journal configuration, and evaluated wherever a balance is needed. No batch window, no reconciliation step, no derived table to keep in sync with the source.

]]>
mgl accounting general-ledger multi-currency devtools
<![cdata[iso 8583:2023 Datasets in jPOS]]> https://jpos.org/blog/2026/03/iso8583-2023-datasets-in-jpos https://jpos.org/blog/2026/03/iso8583-2023-datasets-in-jpos 2026年3月28日 11:20:00 GMT Since ISO 8583:2003, the standard has defined three types of data elements: primitive, constructed, and composite. Primitive and constructed fields have always had first-class support in jPOS. The third type—composite fields—has historically been treated as opaque binary blobs, with application code responsible for parsing their contents manually.

jPOS 3.0.2 changes that.

Background: what composite fields are

ISO 8583:2023 defines several data elements—DE-034, DE-043, DE-049, DE-055, DE-071, DE-104, and others—as composite fields. Rather than having a fixed structure, a composite field contains one or more independent sections called datasets. This design lets a single field carry different categories of information in a flexible, extensible way.

Each dataset follows one of two encoding formats:

TLV datasets (identifier 0x01–0x70): sub-elements are encoded as BER-TLV tag-length-value pairs and can appear in any order. Multi-byte tags following ISO/IEC 7816-6 conventions are supported.

DBM datasets (identifier 0x71–0xFE): sub-elements are indexed by a second-level bitmap (similar in structure to the message bitmap). A DBM dataset can also carry a TLV continuation for rarely-used elements not covered by the bitmap.

Each dataset is preceded by its one-byte identifier and a two-byte big-endian length, giving the full wire structure:

[ id (1 byte) ][ length (2 bytes, big-endian) ][ content ]

DE-055 (ICC/EMV data) is a special case: it carries raw BER-TLV directly, with no dataset identifier or length envelope.

What jPOS now provides

Core classes:

  • Dataset — interface representing a single dataset
  • ISODataset — mutable implementation with fluent builder support
  • DatasetElement — holds one decoded sub-element
  • ISODatasetField — top-level field component that holds one or more datasets

Packagers:

  • DatasetPackager — reads and writes composite fields with full TLV and DBM support
  • ICCDataPackager — special-case packager for DE-055 raw BER-TLV

Message API additions:

  • ISOMsg.with(field, value) — set and return this for fluent chaining
  • ISOMsg.without(field...) — unset and return this for fluent chaining
  • Path-based access for dataset elements via msg.with("55.0x9F26", bytes) / msg.without("55.0x9F26")

Packager:

  • cmfv3.xml — a CMF packager that enables dataset-aware handling for DE-034, DE-043, DE-049, DE-055, DE-071, and DE-104. The original cmf.xml is unchanged for backward compatibility.

Building a message

With cmfv3.xml, you can build and parse composite fields using the same dot-path style used for nested sub-messages:

GenericPackager packager =newGenericPackager("jar:packager/cmfv3.xml");

ISOMsg msg =newISOMsg("0100");
msg.setPackager(packager);

// Dataset paths are: field.datasetId.elementId
msg.with(3,"000000")
.with(11,"123456")
.with(41,"TERMID01")
// ICC data (DE-055): field.elementTag — no dataset wrapper for EMV
.with("55.0x9F26",ISOUtil.hex2byte("1122334455667788"))
.with("55.0x9F10",ISOUtil.hex2byte("06011203A0B800"))
.with("55.0x9F36",ISOUtil.hex2byte("0022"))
.with("55.0x95",ISOUtil.hex2byte("0000000000"))
// Verification data (DE-049): TLV dataset 0x01
.with("49.0x01.0x5F2A",ISOUtil.hex2byte("0840"))
// Verification data (DE-049): DBM dataset 0x71
.with("49.0x71.1","1")
.with("49.0x71.2","1234");

byte[] packed = msg.pack();

Reading values back after unpacking:

ISOMsg unpacked =newISOMsg();
unpacked.setPackager(packager);
unpacked.unpack(packed);

ISODatasetField field55 =(ISODatasetField) unpacked.getComponent(55);
ISODataset icc =(ISODataset) field55.getDataset(55);// keyed by field number for ICC

byte[] cryptogram = icc.getBytes(0x9F26);// Application Cryptogram
byte[] iad = icc.getBytes(0x9F10);// Issuer Application Data

ICC data backward compatibility

If you already have ICC data as raw TLV bytes from the legacy cmf.xml packager, cmfv3.xml produces byte-for-byte identical output. This is verified directly in the test suite:

// Legacy CMF: set DE-055 as raw bytes
ISOMsg legacyMsg =newISOMsg("0100");
legacyMsg.setPackager(newGenericPackager("jar:packager/cmf.xml"));
legacyMsg.set(newISOBinaryField(55, rawTLVBytes));
byte[] legacyPacked = legacyMsg.pack();

// CMFv3: set DE-055 using the fluent path API
ISOMsg datasetMsg =newISOMsg("0100");
datasetMsg.setPackager(newGenericPackager("jar:packager/cmfv3.xml"));
datasetMsg.with("55.0x9F26",ISOUtil.hex2byte("1122334455667788"))
.with("55.0x9F10",ISOUtil.hex2byte("06011203A0B800"))
.with("55.0x9F36",ISOUtil.hex2byte("0022"))
.with("55.0x95",ISOUtil.hex2byte("0000000000"));
byte[] datasetPacked = datasetMsg.pack();

assertArrayEquals(legacyPacked, datasetPacked);// identical wire bytes

Existing systems continue to interoperate without any changes.

Clone safety

ISOMsg.clone() now deep-clones dataset fields, so modifying a cloned message does not affect the original. The without() method also auto-removes the parent field when all its dataset elements are cleared:

ISOMsg clone =(ISOMsg) original.clone();

clone.without("55.0x9F10")// remove one ICC element
.with("55.0x95", zeroes);// add another

// Field 55 in clone is independent of field 55 in original
assertNotSame(original.getComponent(55), clone.getComponent(55));

Getting started

Switch your packager from cmf.xml to cmfv3.xml:

<beanid="packager"class="org.jpos.iso.packager.GenericPackager">
<propertyname="configuration">
<value>jar:packager/cmfv3.xml</value>
</property>
</bean>

Existing code that treats composite fields as opaque binary fields continues to work unchanged with cmf.xml. No migration is required unless you want to take advantage of structured access.

What's next

The DatasetPackager is designed to be subclassed. You can define custom packagers with their own set of bitmap sub-elements for any composite field used in a specific network or scheme.

DE-018 (Message Error Indicator), which uses the same dataset addressing model to report parsing errors, will be wired up in a subsequent release.

]]>
jpos iso8583 datasets emv icc
<![cdata[security, release anxiety, and scanner noise]]> https://jpos.org/blog/2026/03/security-release-anxiety-and-scanner-noise https://jpos.org/blog/2026/03/security-release-anxiety-and-scanner-noise 2026年3月26日 16:53:00 GMT Security conversations around payment systems often suffer from two opposite pathologies: panic and complacency.

Complacency is obviously dangerous. But panic is expensive too. Teams start treating version labels, scanner output, and policy shortcuts as if they were a substitute for engineering judgment. They rush suppliers into unnecessary releases, create change-management noise, and sometimes end up with more operational risk, not less.

This post is about a calmer approach.

1. The problem: scanner noise creates release panic

The failure mode is familiar.

A scanner lights up a dependency in red. The report says CRITICAL. Someone forwards it to management. The supplier gets an urgent request for a new jPOS final release. Change windows get compressed. Engineering judgment disappears.

That is not security. That is dashboard-driven operations.

Dependency scanners answer a worst-case question:

"Can this component be vulnerable somewhere under some conditions?"

Production engineering has to answer a different question:

"Is this system actually exposed here, now, in this environment?"

Those are not the same question, and confusing them is how teams end up doing high-risk work for low-value findings.

2. Why this is dangerous

Panic patching has its own incident history.

A rushed dependency upgrade can break message flows, change serialization behavior, alter TLS defaults, trigger classpath conflicts, or pull in a new transitive tree that was never qualified in your environment. In regulated systems, the blast radius is larger because every rushed change also creates test pressure, approval pressure, and rollback pressure.

The risk is not hypothetical:

  • not patching may leave a real exposure in place
  • patching blindly may create an outage, regression, or unstable supply-chain state

Mature teams evaluate both sides. They do not assume "change" is automatically the safer option.

3. What "critical" actually means

A vulnerability in a library is not automatically a critical vulnerability in your system.

Component severity is often assigned without knowing:

  • whether the vulnerable code path is reachable
  • whether the feature is enabled
  • whether the system is internet-facing
  • whether an attacker can provide the required input
  • whether exploitation requires prior authentication or elevated privilege
  • whether compensating controls already block the attack path

That context matters more than the scanner color.

If a parser bug only matters when an attacker can feed crafted input into a reachable interface, and in your deployment that parser is only used for privileged internal configuration during controlled startup, then the component finding may be real while the system-level exploitability is negligible.

That is not denial. That is classification.

4. Decision framework: can you safely defer this finding?

I would avoid the phrase "ignore" because auditors hate it and because it encourages sloppy thinking. The real question is whether you can defer, override, or accept the risk with evidence.

Use this checklist in order:

  1. Confirm the finding.
    • Is the artifact correct?
    • Is the version range correct?
    • Is this a real match, or scanner confusion around shading, relocation, or backports?
  2. Check reachability.
    • Is the vulnerable function actually on an executed code path?
    • Is the feature enabled in your deployment?
  3. Check exposure.
    • Is the path internet-facing, partner-facing, internal only, or admin-only?
    • Can an attacker supply the triggering input directly?
  4. Check exploit conditions.
    • Is there public exploit code?
    • Is exploitation theoretical, demonstrated, or actively weaponized?
    • Does it require authentication, local access, or elevated privilege?
  5. Check compensating controls.
    • Authentication
    • Network segmentation
    • WAF or protocol filtering
    • Input validation
    • Operational approvals and restricted admin access
  6. Check change risk.
    • Does the patch require a major version jump?
    • Will it alter runtime behavior in a payment path?
    • Has it been qualified in your environment?

Use the answers to make a decision:

  • Patch now if the code path is reachable, externally exposed, low-privilege to exploit, and there is weak or no compensating control.
  • Patch soon or override the dependency if the issue is real but exposure is narrower and the remediation is low risk.
  • Defer with documented risk acceptance if the vulnerable path is not reachable, requires privileged access you already tightly control, or is materially blocked by compensating controls.

That last bucket is where many "critical" findings belong, and that is precisely why severity labels should not drive release policy by themselves.

Also: you do not always need a new upstream jPOS release. If the issue is a transitive dependency and jPOS is otherwise compatible with the fixed library version, a dependency override in Maven or Gradle is often the lightest effective remediation.

That is not a hack. That is basic dependency hygiene, and it is one of the reasons build tools exist.

Example: overriding a flagged jPOS transitive dependency

Suppose a scanner flags jackson-core 2.20.1 pulled transitively by jPOS 3.0.1, and your policy requires consuming a newer approved version.

That does not automatically mean you need to stop and wait for a new jPOS final release. If your qualification shows jPOS works correctly with the newer jackson-core, you can override it in your own build and move forward.

Gradle

dependencies {
implementation("org.jpos:jpos:3.0.1")
}

configurations.all {
resolutionStrategy.eachDependency { details ->
if(details.requested.group =="com.fasterxml.jackson.core"&&
details.requested.name =="jackson-core"){
details.useVersion("2.21.2")
details.because("Override transitive dependency to approved version")
}
}
}

If you prefer constraints:

dependencies {
implementation("org.jpos:jpos:3.0.1")

constraints {
implementation("com.fasterxml.jackson.core:jackson-core:2.21.2"){
because("Use approved jackson-core version")
}
}
}

Maven

<dependencies>
<dependency>
<groupId>org.jpos</groupId>
<artifactId>jpos</artifactId>
<version>3.0.1</version>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.21.2</version>
</dependency>
</dependencies>
</dependencyManagement>

Or declare it directly:

<dependencies>
<dependency>
<groupId>org.jpos</groupId>
<artifactId>jpos</artifactId>
<version>3.0.1</version>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.21.2</version>
</dependency>
</dependencies>

5. Compliance reality: PCI DSS, audits, and scanner findings

Regulated environments do not give you permission to think less. They require you to think more clearly and document it better.

PCI DSS, external scans, and audit programs create pressure, but they do not eliminate the difference between a false positive, a non-reachable finding, and a materially exploitable defect. The mistake is treating every scanner result as self-proving.

What auditors usually need is not panic. They need a defensible record:

  • the advisory or CVE identifier
  • the affected component and version
  • whether the finding is confirmed or false positive
  • reachability and exposure analysis
  • compensating controls in place
  • business impact if exploited
  • remediation decision: patch, override, defer, or accept
  • owner, approval date, and review date

What to tell an auditor:

"The scanner detected component X and mapped it to CVE-Y. We validated the version and reviewed exploitability in this deployment. The vulnerable function is not reachable from external interfaces, exploitation requires privileged access, and compensating controls include authentication, segmented network access, and controlled configuration management. We have documented the risk, assigned an owner, and scheduled review at the next qualified release window."

That is a serious answer. "The scanner is noisy" is not.

If you need an exception, write it like an engineer, not like a lawyer hiding a problem. Be explicit about the conditions that make the risk acceptable and the trigger that would change the decision, such as public weaponization, changed exposure, or a low-risk patch becoming available.

That is how you avoid checkbox security without picking a fight with compliance.

6. A jPOS SNAPSHOT does not mean untested

One of the more persistent misconceptions is that a jPOS SNAPSHOT means "random untested code" while a final release means "safe."

That is nonsense.

In a disciplined project, a jPOS snapshot and a jPOS final release can come from the same codebase, pass the same CI pipeline, and differ mostly in publication semantics. If the build is reproducible, the commit is pinned, the artifact is immutable in your internal repository, and you run your own qualification, a snapshot is not inherently reckless.

A snapshot is acceptable when all of this is true:

  • the exact build is pinned to a commit or immutable artifact hash
  • CI and tests are the same standard you would expect for a release
  • you can reproduce or verify the build provenance
  • your team performs its normal validation before production
  • rollback is defined

It is not acceptable when:

  • "snapshot" means floating latest
  • nobody can tell which commit produced the artifact
  • the build is not reproducible
  • the artifact can change underneath the same coordinate
  • the team is using it to skip qualification rather than accelerate it

The real comparison is not jPOS SNAPSHOT versus "perfectly blessed release." It is often pinned, tested snapshot versus rushed jPOS release cut under pressure.

And in that comparison, the rushed release is frequently the riskier object. It may include unrelated changes, abbreviated testing, approval theater, and false confidence created by a prettier version label.

A version suffix is not a security property.

7. Two practical examples

Example 1: "Critical" CVE in an unused logging library

Your scanner flags a CRITICAL issue in a logging dependency. The affected function is a network appender your deployment does not enable. Logs are written locally, configuration is managed through infrastructure code, and only privileged operators can change it.

Framework result:

  • reachability: no
  • exposure: no external path
  • exploit maturity: irrelevant without reachability
  • privilege required: privileged configuration access
  • compensating controls: strong
  • change risk: moderate, because the upgrade drags a wider logging stack refresh

Decision: defer or accept with documentation, then upgrade in a normal release window. If an attacker can already alter production logging configuration, the logging CVE is not your primary problem.

Example 2: vulnerable transitive parser on a non-executed path

A transitive dependency has a parser vulnerability. The scanner reports HIGH. In your deployment the parser is only used by an admin-only batch import tool, not by the online transaction path. The batch function sits behind VPN, SSO, role-based access control, and change approval.

Framework result:

  • reachability: reachable only through admin workflow
  • exposure: not internet-facing
  • exploit maturity: public proof of concept exists
  • privilege required: authenticated admin access
  • compensating controls: strong
  • change risk: low, because you can override the transitive dependency cleanly

Decision: do not demand an emergency jPOS release. Override the dependency in your own build, qualify it, document the interim risk posture, and move on.

8. Conclusion

Security work gets worse when people outsource judgment to scanner colors, version suffixes, and ceremony.

Patch real risk quickly. Defer low-value findings with evidence. Use dependency overrides when that is the cleanest fix. Accept that a pinned, tested jPOS snapshot can be safer than a rushed final release. And stop pretending that "critical in a library" automatically means "critical in production."

Good security engineering is not about reacting faster to noise. It is about being able to prove why something matters, why something does not, and what trade-off you are making when you change a running system.

]]>
jpos security dependencies maven gradle
<![cdata[pci DSS and the AI Agent Era]]> https://jpos.org/blog/pci-ai-agent-era https://jpos.org/blog/pci-ai-agent-era 2026年3月25日 00:00:00 GMT PCI DSS version 4.0.1 was published on June 11, 2024. That's less than two years ago. It already feels like a different industry.

In that time, AI agents have gone from research curiosity to production infrastructure. The Model Context Protocol gave language models a standard way to call tools. Autonomous agents that read messages, query databases, push commits, and manage tasks are now deployed in organizations running payment systems.

The standard has nothing to say about any of this.

What PCI SSC has published

To its credit, the PCI Security Standards Council has moved. In March 2025 it published Integrating Artificial Intelligence in PCI Assessments—its first AI-related guidance document. It's a serious piece of work.

But it addresses a narrow question: how should QSAs use AI tools during assessments? It says AI is a tool, not an assessor. It requires human oversight. It asks assessors to disclose AI use to clients and to document their policies. All reasonable.

What it doesn't address is the question payment practitioners are actually wrestling with: what happens when you deploy an AI agent in your own environment?

The gap

An AI agent in a payment environment is not an abstract risk. It is a concrete software system with:

  • credentials to external services (Mattermost, GitHub, Asana, deployment systems)
  • a persistent workspace with memory, files, and context
  • the ability to make tool calls—API requests, shell commands, file operations—based on natural-language instructions
  • access to data streams that may touch the cardholder data environment

PCI DSS was designed around the concept of human users and service accounts. Requirements 7 and 8 define access control and authentication in those terms. An AI agent is neither a human nor a conventional service account—it is an autonomous system capable of initiating actions across multiple systems, adapting to responses, and being manipulated through its inputs.

This is new territory, and the standard doesn't map to it cleanly.

The workstation problem

The most dangerous AI agent deployment pattern I've seen is also the most common: an agent running on an operator's personal workstation.

It starts innocuously. Someone installs an AI assistant on their laptop, connects it to their credentials, and begins using it. The assistant is useful, so it gets more access. Before long, it's running with an active SSH agent loaded with private keys, a VPN session open to internal networks, browser cookies and stored tokens, and access to cloud storage and development databases.

From a PCI standpoint, this is a significant finding waiting to be written. A prompt injection attack—malicious instructions embedded in data the agent processes, like a Mattermost message or a task description—could leverage all of those credentials. The blast radius is the same as a fully compromised operator workstation, but the attack surface is broader because the agent processes untrusted external data continuously.

The fix is architectural: agents that touch production environments must run on dedicated hosts with dedicated identities. Not a developer's laptop. Not a shared server. A machine provisioned for that purpose, with scoped credentials, no loaded SSH keys beyond what the agent specifically requires, and no active VPN sessions outside its declared scope.

Messaging channels are not equivalent

When an AI agent communicates with a human operator, the choice of channel matters for PCI purposes.

Signal is end-to-end encrypted by default. Telegram encrypts messages in transit but stores them on vendor servers by default (end-to-end encryption requires Secret Chats, which aren't available for bots). WhatsApp stores messages on Meta's infrastructure and is subject to Meta's data retention practices—if operational data touching the CDE passes through it, Meta enters scope as a Third Party Service Provider.

This is not a hypothetical. Instructions to an AI agent are operational data. If those instructions cause the agent to take actions in a payment environment, the channel carrying those instructions is relevant to PCI scope analysis.

MCP servers are a new attack surface

The Model Context Protocol changed the shape of this problem. MCP servers expose tools—file access, database queries, API calls, shell execution—to a language model. When an MCP server is deployed with access to a payment system, the language model invoking it inherits that access.

From a Requirement 6 perspective, MCP server software must be treated as a system component: included in the SBOM, subject to vulnerability scanning, and developed with input validation applied to tool parameters—because the language model calling those tools is, from a security standpoint, an untrusted input source.

Prompt injection in payment contexts

Prompt injection is the AI-era equivalent of SQL injection. An attacker who can influence the data an AI agent processes can sometimes influence what it does.

In a payment environment, the surfaces for injection are everywhere: Mattermost channels, task descriptions, document contents, API responses. An agent that reads external data and takes consequential actions based on it is exposed to this class of attack.

The best mitigation is scope restriction. An agent that can only read cannot exfiltrate data or modify systems even if successfully injected. Every write operation—every action that creates, modifies, or deletes data in a production system—should require explicit human confirmation. Not because the agent is untrustworthy, but because the inputs it processes are.

What practitioners should do now

The standard will catch up. Standards always do, eventually. In the meantime, a practical baseline for AI agents in payment environments:

  • Dedicated host. Agent runs on its own machine, not a workstation.
  • Dedicated identity per system. Separate accounts for every service the agent accesses. No sharing credentials with human users.
  • Minimal credential scope. Read-only tokens where possible. Write access documented and approved.
  • Approved messaging channel. Evaluate the channel. Signal or Telegram (with appropriate controls) for operational use; WhatsApp for environments where Meta becoming a TPSP is unacceptable.
  • LLM provider as TPSP. The provider running inference receives your prompts. Review their data processing agreement. Opt out of training data use. For highest-sensitivity deployments, consider a self-hosted model.
  • Audit trail. All tool calls logged: timestamp, tool, parameters (sanitized), outcome. These belong in the audit record.
  • Human-in-the-loop for writes. Any action that modifies production data requires explicit confirmation.

On the pciguide

If you are building jPOS-based payment infrastructure, we've added detailed guidance on this topic to the jPOS PCI DSS Compliance Guide. Requirement 12 now includes a full section on AI agents as a new category of privileged system, covering deployment architecture, messaging channel evaluation, LLM provider TPSP classification, and a governance controls table. Requirement 6 addresses MCP server security requirements and prompt injection as a vulnerability class.

The industry is figuring this out in real time. The more practitioners who publish what they're actually doing, the faster the collective practice advances.

]]>
pci security ai jpos payments
<![cdata[pci DSS 4.0.1 Compliance Guide for jPOS-Based Systems]]> https://jpos.org/blog/pci-dss-guide-jpos https://jpos.org/blog/pci-dss-guide-jpos 2026年3月24日 00:00:00 GMT Roughly every other day, a jPOS-based system achieves PCI DSS certification somewhere in the world. That statistic is not accidental—it reflects decades of work building a framework designed from the ground up for the specific demands of payment security. It also means that Transactility has more direct experience with jPOS PCI assessments than any consulting firm, QSA, or system integrator on the market.

This guide is the result of that experience, made freely available to the entire jPOS community.

What It Is

The PCI DSS 4.0.1 Compliance Guide for jPOS-Based Systems is an 850-page, audit-ready reference document that maps every requirement, sub-requirement, and sub-sub-requirement of PCI DSS 4.0.1 to concrete jPOS implementations. It covers the full scope of The Payment Platform—jPOS, jPOS-EE, jCard, jPTS, and tpp-commons—and extends to supply chain security, cryptographic key management, and operational governance.

Each section is written for a mixed audience: developers who need implementation specifics, CISOs who need governance evidence, and QSAs who need to understand how controls are realized in practice. Every requirement includes three perspectives—customer, auditor, and adversary—so the guidance is grounded in how controls actually get tested and, more importantly, how they actually get attacked.

The guide includes:

  • Full requirement coverage — every PCI DSS 4.0.1 requirement, sub-requirement, and note, addressed in detail
  • jPOS-specific implementation guidance — CryptoService, TokenizationService, HSM integration, Q2 logging, transaction participant patterns
  • Policy template library — over 50 ready-to-customize policy templates covering access control, key lifecycle, incident response, patch management, and more
  • Supply chain security framework — aligned with ISO/IEC 20243 (O-TTPS), covering SBOM lifecycle, dependency scanning, CI/CD hardening, and CVE response SLAs
  • Cross-standard alignment — mapped to ISO/IEC 27001, NIST SP 800-57, NIST SP 800-63B, and ISO/IEC 20243 throughout
  • Diagrams-as-code — network topology documentation guidance designed for Git-based version control and change management

Download

Download the PCI DSS Guide (PDF)

The document is free to use, adapt, and build on. Use the policy templates as a starting point, replace the Transactility branding with your own, and tailor the jPOS-specific guidance to your deployment. No strings attached.

When You Need More Than a Document

Reading a compliance guide and operating a secure payment system are two different things. If your organization is preparing for a PCI DSS assessment, responding to a security incident, or building a payment platform and wants the team that built jPOS directly involved—in architecture reviews, policy development, QSA preparation, or implementation—we are available.

Nobody knows the jPOS codebase, its security model, or its real-world failure modes the way the people who built it do.

When it matters most, bring the core team into the room.
transactility.com

]]>
pci-dss security compliance jpos transactility
<![cdata[jpos Log Viewer: structured operational logs]]> https://jpos.org/blog/logview-demo https://jpos.org/blog/logview-demo 2026年3月22日 20:00:00 GMT Most log viewers are built around the assumption that logs are lines of text. Search is grep. Filtering is awk. Correlation is copy-pasting timestamps and hoping for the best. The infrastructure to make that tolerable — log shippers, ingestion pipelines, index clusters — adds cost and complexity, and the result is still a flat text interface.

jPOS' Log Viewer takes a different approach, and it can do so because jPOS's logging is different at the source.

Structured from the start

jPOS has always had a typed, structured event model at its core. Every log event is a self-describing object—not a formatted string, but a record with well-defined fields: timestamp, kind, realm, host, trace identifier, and a typed payload that varies by event type.

This wasn't retrofitted onto the system. It's the foundation. When jPOS 3.0.0 was released, structured audit logging was formalized as a first-class feature—typed events with PCI-aware field protection, flat metadata, and a JSONL serialization that makes log events portable without losing structure.

The Log Viewer indexes these structures directly. That's what makes faceted search, histogram drill-down, and trace correlation possible without any additional transformation layer.

What the demo shows

The video above walks through the viewer from an operator's perspective:

Histogram and time navigation. The event volume histogram at the top is interactive. Clicking a bar zooms into that time window—the date range updates and results narrow to that slice. During incident investigation, this is the fastest way to go from "something happened around 14:30" to "here are all the events in that two-minute window."

Faceted filtering. Column headers act as facet controls. Clicking Kind opens a checkbox dropdown for event types—info, warn, send, receive, deploy, and so on. Clicking Realm narrows to a specific subsystem. Facets compose: combining kind and realm gives an operator exactly the signal they need, without writing queries.

Full-text search with Lucene syntax. The search box accepts field-specific queries: kind:send OR kind:receive, realm:channel, or plain text matched across the payload. Results update immediately.

Multi-event comparison. Selecting multiple rows opens them in a modal with a Plain tab and a JSON tab. The Plain view shows time deltas between events—you can read the exact duration from channel receive through transaction manager processing to response. The JSON tab exposes the raw structured source, ready to copy into an incident report or pipe into another tool.

Trace correlation. Many events carry a trace identifier that spans the entire lifecycle of a request. Clicking the trace link on any row filters to all events sharing that trace—the full journey from channel to transaction manager and back, in one view.

Offline forensic import. The DevTools workspace includes a log ingestor. Drag and drop JSONL log files—from production, from staging, from a colleague's environment—and analyze them with the same faceted search, histogram, and trace tools. Multiple files can be imported together.

Why it works

Generic log aggregation tools treat every system the same way. They expect text, they parse it heuristically, and they provide generic search. That works, but it stops there.

Because jPOS' Log Viewer is built on top of jPOS's typed event model, it can go further. A tag carrying a transaction ID can link directly to data in the general ledger. A deploy event renders with the full list of deployed components, not a raw XML blob. A channel event shows connection state and timing in a purpose-built renderer.

This is the practical difference between indexing structured data and parsing text that was never meant to be parsed.

More to come

The viewer is already useful for operations and debugging. The next steps are deeper integration with jPOS' transaction data — linking log events to their corresponding GL postings, surfacing cost and timing information in context, and making the operational picture and the financial picture visible from the same interface.

]]>
logging devtools jpos
<![cdata[jpos upgraded to Java 26]]> https://jpos.org/blog/jpos-java26 https://jpos.org/blog/jpos-java26 2026年3月20日 08:00:00 GMT Java 26 reached General Availability on March 17, 2026. jPOS, jPOS-EE, and the tutorial projects have been updated to run on it, alongside Gradle 9.4.1.

Notable in this release:

  • HTTP/3 support in the standard HttpClient API (JEP 517)
  • Ahead-of-Time Object Caching with any GC (JEP 516), improving startup latency
  • G1 GC throughput improvements via reduced synchronization (JEP 522)
  • PEM encoding of cryptographic objects (JEP 524, second preview)
  • Structured Concurrency continues to mature (JEP 525, sixth preview)
  • Applet API removed (JEP 504)

No source changes were required in jPOS or jPOS-EE. The tutorial projects needed a minor import path correction: ISOUtil had moved from org.jpos.util to org.jpos.iso in an earlier jPOS release and the tutorials hadn't caught up yet.

]]>
jpos java
<![cdata[massivegl FX Rates]]> https://jpos.org/blog/massivegl-fx-rates https://jpos.org/blog/massivegl-fx-rates 2026年3月17日 21:56:00 GMT One of the things that breaks down quickly in real-world accounting systems is currency.

Most ledger designs treat currency as an afterthought — a field on a transaction, a conversion applied at reporting time, a problem deferred to the spreadsheet team. MGL treats it differently. Exchange rates are a first-class part of the data model, imported automatically, stored with full history, and wired directly into the layer architecture so that multi-currency consolidation happens at query time, not at batch time.

What the demo shows

The short demo above walks through the FX module from the perspective of someone setting up and operating a live system:

  • log in and navigate to the FX Rates page
  • browse all stored rates across every currency pair in the system
  • filter by pair — USD/UYU, EUR/USD, EUR/UYU — and see the rate history chart for each
  • switch to the posting console to see how layers and FX rates work together

It is a deliberately short tour. The interesting part is not the UI — it is what the UI is sitting on top of.

Rates, stored and queryable

MGL imports rates from two sources out of the box: the Frankfurter API, which sources data from the European Central Bank, and the Banco Central del Uruguay for USD/UYU and EUR/UYU pairs. Both importers run continuously in the background, backfilling historical data and polling for new rates at a configurable interval.

Each rate is stored with its currency pair, date, rate value, and source identifier. The unique constraint on (pair, date) means upserts are atomic and idempotent — you can run importers in parallel without creating duplicates or races.

The most recent stored date is always available to each importer, so if the system goes offline for a few days it will automatically resume backfilling from where it left off, without manual intervention.

Adding a new rate source is also straightforward: implement a QBean that calls FXRateManager.upsert() with your pair, date, rate, and a source identifier. The rest of the system picks it up automatically.

Where the rates actually get used

The real reason to care about FX rates in MGL is virtual layers.

Physical layers in a journal hold actual posted entries — transactions with real debit and credit amounts in a specific currency. Virtual layers hold no entries. Instead, they carry a formula that computes a balance on demand from other layers, and that formula can invoke fx() to convert amounts using a rate from the database.

So if a journal has three physical layers — layer 0 for Uruguayan pesos, layer 840 for US dollars, layer 978 for euros — a single virtual layer can consolidate all three into a reporting currency:

L0 + fx(L840, "USD/UYU") + fx(L978, "EUR/UYU")

When you query that virtual layer's balance for a given date, the engine reads the balances of the three physical layers, looks up the exchange rates for that date, applies the conversions, and returns the sum. The original postings are never touched. The ledger stays clean. You get a consolidated balance that reflects the actual exchange rates in effect on any historical date you care to query.

This matters for two reasons. First, it removes the need for re-valuation entries or synthetic postings whenever rates change. Second, it means you can go back to any point in time and get an accurate consolidated view using the rates that were actually in effect that day, not today's rates applied retroactively.

Inverse pairs come for free

One small but practical detail: if you store a rate for USD/UYU, the engine automatically makes UYU/USD available as 1 / rate. You only need to store rates in one direction. The inverse lookup is handled in the DBFXProvider, which is the component that GLSession uses when evaluating virtual layer formulas.

Reliable, not magical

There is nothing glamorous about exchange rate management. But it is the kind of thing that quietly breaks accounting systems that were not designed for it from the start.

MGL's approach is simple: import rates from authoritative sources, store them with full history, handle gaps by falling back to the most recent available rate, and make them directly addressable from the layer formula language. No spreadsheets, no manual lookups, no batch jobs that need to run before month-end close.

More to come as the virtual layer documentation and formula reference take shape.

]]>
mgl accounting multi-currency fx general-ledger
<![cdata[massivegl Is Taking Shape]]> https://jpos.org/blog/massivegl-taking-shape https://jpos.org/blog/massivegl-taking-shape 2026年3月17日 00:00:00 GMT MGL is starting to feel like a real system now, not just a collection of ideas.

The core ledger is already in place, but what makes it interesting is how the pieces are starting to come together: multi-layer accounting, virtual layers for reporting and FX-driven consolidation, high-volume controller-style account structures, and an AI assistant that can operate the system through the same tools and permission model used by the web UI.

The short demo above starts from a blank system and goes through the basic flow:

  • create an entity
  • create a journal
  • select a standard chart
  • define a layer for USD
  • ask the AI assistant to generate a usable chart of accounts
  • ask the AI assistant to prepare a draft opening balance transaction
  • review, post, and reverse transactions

That flow matters because it shows the direction of the project. MGL is not starting from scratch. It is based on miniGL, a previous generation that, despite the "mini" name, has been used in production as the system of record for very large jCard deployments, including at least two known cases with more than 1.5 billion cards on file.

So the point of MGL is not to prove that a ledger can exist. That part was settled a long time ago. The point is to address the pain points observed in those very large systems, while pushing the model forward with better layering, better operational tooling, and better integration points, including AI-assisted workflows.

Virtual layers

One of the most interesting features is the virtual layer support.

Physical layers let us post independent values inside the same journal, typically by currency or reporting dimension. But virtual layers are where things get really interesting: instead of storing entries, they compute balances on demand from formulas.

That gives us a very clean way to build consolidated reporting views. You can keep native postings in their original layers and then define a virtual layer that combines them, applies FX rates, or derives reporting values without polluting the base ledger with synthetic entries.

This keeps the original postings intact while still making it possible to answer questions such as:

  • what is the balance of this account in a reporting currency
  • what does a consolidated view look like across several operating layers
  • how did that view look on a specific historical date

That's an important distinction. This is not just a matter of attaching currencies to entries; layers are treated as a first-class accounting dimension, and virtual layers make it possible to compute on top of that model.

Controlled accounts at scale

Another important piece is support for controlled accounts, implemented in the engine through controller-style account structures.

Traditional chart hierarchies are fine for ordinary financial statements, but they start to hurt when you need to group very large numbers of subsidiary accounts. Customer wallets, merchant positions, vendor balances, or other high-cardinality structures need something more scalable than a tree that eagerly loads children.

MGL's controller-oriented model is meant for exactly that scenario. The parent can represent the controlled aggregate, while the underlying final accounts can grow into the thousands or millions without turning balance queries into a disaster. That opens the door to using the same ledger engine both for classical accounting and for much more operational, high-volume sub-ledger work.

This is one of the areas where the project starts to separate itself from simpler GL implementations.

AI that actually does useful work

The AI integration is also becoming much more concrete.

The demo is not using AI as a decorative chatbot. It is using the assistant to do useful ledger work:

  • generate a chart of accounts
  • create bank accounts in the right place
  • draft a balanced opening transaction
  • populate the posting console for human review

What matters here is not the chat UI itself, but the execution model behind it. The assistant uses the same backend tools and respects the same authorization model as the rest of the application. That means the AI is not a side channel; it is another client of the platform, with the same need for permissions, validation, and auditability.

This is the only way AI belongs in systems like this. It has to help operators move faster without bypassing the controls that make the ledger trustworthy in the first place.

More shape, less hand waving

There is still a lot to do, of course, but the project already has a recognizable center of gravity:

  • a real double-entry engine
  • journals with explicit layers
  • virtual layers for reporting and FX conversions
  • controller-style support for large controlled account populations
  • transaction templates
  • reversals and transaction groups
  • AI-assisted setup and posting

That is enough to start seeing where MGL wants to go.

The old miniGL idea was always useful, but this is starting to feel like something much broader. MassiveGL is a better name for the current ambition.

More demos and documentation will follow as the model continues to settle down.

]]>
mgl accounting ai general-ledger
<![cdata[jpos Gradle Plugin 0.0.17]]> https://jpos.org/blog/jpos-gradle-plugin-0.0.17 https://jpos.org/blog/jpos-gradle-plugin-0.0.17 2026年3月16日 00:00:00 GMT We just released version 0.0.17 of the jPOS Gradle Plugin. This release brings two meaningful improvements and a toolchain upgrade.

extraPaths for distnc / zipnc tasks

The distnc and zipnc tasks produce a "no-config" distribution archive — binaries and JARs, without environment-specific cfg/ or deploy/ files. That's useful for packaging the immutable part of your app separately from configuration.

Until now those tasks only included src/dist/bin/. If your project ships additional static assets — documentation, HTML files, static resources, or web content — they were silently left out.

You can now declare extra directories to include:

jpos {
extraPaths =['html','docs','webroot']
}

Each entry is a path relative to src/dist/. Token replacement and the same binary-file handling (images, PDFs, WARs are copied raw; text files go through @token@ substitution) apply to extra paths exactly as they do to the rest of the distribution.

Git repository discovery fix

GitRevisionTask — the task that writes revision.properties — previously used Git.open(projectDir), which requires the Gradle project root to be exactly the git repository root. This caused silent revision=unknown output (or worse, task failures) in multi-module setups where the Gradle root sits inside a larger repository.

The task now uses FileRepositoryBuilder.findGitDir(), which walks up the directory tree to locate .git, exactly the way the git CLI itself does. Worktrees and nested modules all work correctly. And when no git repository is found at all (non-git projects), the task now logs a WARN instead of silently swallowing the error.

Toolchain upgrade

The plugin itself now builds with Java 25.0.2-amzn and Gradle 9.4.0.

How to upgrade

plugins {
id 'org.jpos.jposapp' version '0.0.17'
}
]]>
jpos gradle plugin release
<![cdata[jpos-ee Tip and Tail]]> https://jpos.org/blog/2026/03/jposee-tip-and-tail https://jpos.org/blog/2026/03/jposee-tip-and-tail 2026年3月11日 00:00:00 GMT
REQUIRED ACTIONS

If you already have a local copy of jPOS-EE (master or next), please note there are REQUIRED ACTIONS at the end of this blog post.

Following the same branch model we adopted for jPOS last December, jPOS-EE is now aligned with JEP-14 — The Tip & Tail Model of Library Development.

The rename is straightforward:

  • The next branch (jPOS-EE 3.x) becomes main — the tip, where active development happens.
  • The master branch (jPOS-EE 2.x) becomes tail — the stable series, receiving only critical fixes.

REQUIRED ACTIONS

On your existing next branch:

git branch -m next main
git fetch origin
git branch -u origin/main main
git remote set-head origin -a

On your existing master branch:

git branch -m master tail
git fetch origin
git branch -u origin/tail tail
]]>
jpos-ee jep-14

AltStyle によって変換されたページ (->オリジナル) /