A committed/voided transaction permanently reports inflight: true
#293 opened on May 18, 2026
Repository metrics
- Stars
- (475 stars)
- PR merge metrics
- (PR metrics pending)
Description
Bug: a committed/voided transaction permanently reports inflight: true
Type: bug
Component: inflight_transaction.go — finalizeCommitment, finalizeVoidTransaction
Version checked: main @ e0f8676 (2026-05-18)
Summary
When an inflight transaction is committed (or voided), the resulting
terminal transaction has status: APPLIED (or VOID) but still
reports inflight: true. The wrong value is returned by the
commit/void call and by every subsequent GET /transactions/{id}
and GET /transactions/reference/{ref} — it is not transient.
A transaction cannot be both APPLIED and inflight. Any consumer
that classifies a transaction by the inflight flag (rather than
status) will misjudge a settled transaction as still in flight.
Root cause
inflight is not a column on blnk.transactions — it is stored
inside the meta_data JSONB. An inflight transaction's meta_data
therefore contains {"inflight": true, ...}.
finalizeCommitment (inflight_transaction.go:378) builds the
committed child by mutating the inflight transaction in place:
func (l *Blnk) finalizeCommitment(ctx context.Context, transaction *model.Transaction, withQueue bool) (*model.Transaction, error) {
transaction.Status = StatusCommit
transaction.ParentTransaction = transaction.TransactionID
transaction.CreatedAt = time.Now()
...
transaction.TransactionID = model.GenerateUUIDWithSuffix("txn")
transaction.Reference = model.GenerateUUIDWithSuffix("ref")
transaction.Hash = transaction.HashTxn()
...
}
It overrides status / id / reference / hash / parent — but never
touches transaction.MetaData, which still holds inflight: true.
The committed child is then INSERTed with that metadata verbatim.
finalizeVoidTransaction (inflight_transaction.go:566) has the same
omission.
On read, restoreTransactionFlagsFromMetadata (transaction.go:1653)
re-derives the struct field from metadata:
func restoreTransactionFlagsFromMetadata(transaction *model.Transaction) {
if v, ok := transaction.MetaData["inflight"].(bool); ok {
transaction.Inflight = v // <-- a committed txn becomes Inflight=true
}
...
}
So a terminal transaction's meta_data carries inflight: true, and
every read faithfully restores Inflight = true from it.
transformTransaction strips the inflight key out of the response
meta_data but leaves result.Inflight = true.
Steps to reproduce
# 1. Create an inflight transaction.
INF=$(curl -s -X POST http://localhost:5001/transactions \
-H 'X-Blnk-Key: <key>' -H 'Content-Type: application/json' \
-d '{"amount":100,"precision":100,"currency":"USD","reference":"repro-'"$(date +%s)"'",
"source":"<balance_id>","destination":"@World","description":"x",
"inflight":true,"skip_queue":true,"allow_overdraft":true}')
INFID=$(echo "$INF" | jq -r .transaction_id)
# 2. Commit it — inspect the response.
COMMIT=$(curl -s -X PUT "http://localhost:5001/transactions/inflight/$INFID" \
-H 'X-Blnk-Key: <key>' -H 'Content-Type: application/json' \
-d '{"status":"commit"}')
echo "$COMMIT" | jq '{status, inflight}'
# -> { "status": "APPLIED", "inflight": true } ❌ BUG
# 3. GET it back later — still wrong.
CHILDID=$(echo "$COMMIT" | jq -r .transaction_id)
curl -s "http://localhost:5001/transactions/$CHILDID" -H 'X-Blnk-Key: <key>' | jq '{status, inflight}'
# -> { "status": "APPLIED", "inflight": true } ❌ BUG
Confirmed against the database
The committed child's stored meta_data (queried directly from
Postgres) carries the inherited flag — there is no inflight column,
and the value survives a full Redis FLUSHALL, so it is genuinely
persisted, not cached:
txn_<parent> | INFLIGHT | {"inflight": true, "allow_overdraft": true}
txn_<child> | APPLIED | {"inflight": true, "allow_overdraft": true} <-- stale
Expected
A committed transaction should report inflight: false (status
APPLIED); a voided one inflight: false (status VOID). The
inflight flag must reflect the terminal state, not be inherited from
the parent.
Suggested fix
In finalizeCommitment and finalizeVoidTransaction, after the
field overrides, clear the inherited flag from metadata so the
persisted child and every subsequent read are correct:
if transaction.MetaData != nil {
delete(transaction.MetaData, "inflight")
}
Clearing it in meta_data is the right layer because
restoreTransactionFlagsFromMetadata derives the Inflight struct
field from metadata — the metadata is the source of truth, and it is
what is wrong. (Setting transaction.Inflight = false alone would be
overwritten on the next read.)
Note
This is the same class of bug as #291 (prepareRefundTransaction):
a terminal/derived transaction is built by copying a source transaction
and overriding a partial set of fields, leaving one field stale. The
commit/void path and the refund path share this pattern.