blnkfinance/blnk

POST /transactions/filter never returns parent_transaction — the column is omitted from the filter query

Closed

#292 opened on May 18, 2026

 (1 comment) (1 reaction) (0 assignees)Go (129 forks)auto 404
buggood first issue

Repository metrics

Stars
 (475 stars)
PR merge metrics
 (PR metrics pending)

Description

Bug: POST /transactions/filter never returns parent_transaction — the column is omitted from the filter query

Type: bug Component: database/transaction.goGetAllTransactionsWithFilterAndOptions Version checked: main @ e0f8676 (2026-05-18)

Summary

GET /transactions/{id} returns a transaction's parent_transaction correctly. POST /transactions/filter returns the same transaction with parent_transaction as an empty string — even when you filter on parent_transaction and the row matches.

The two endpoints share the transformTransaction serializer, so the divergence is not in serialization — it is in the SQL: the filter query's column list omits parent_transaction entirely, so the scanned struct's field is never populated and serializes as "".

Impact

parent_transaction is the link used to walk a transaction's lifecycle (QUEUED → INFLIGHT → APPLIED/VOID, and refund → original). The documented way to fetch an inflight's terminal children is to filter on parent_transaction (see transactions/parent-transactions.mdx). That exact query returns every child with an empty parent_transaction, so a consumer cannot reconstruct the chain from a filter result — it is forced into an extra GET /transactions/{id} per row to recover the link the filter could have returned.

Root cause

database/transaction.go, GetAllTransactionsWithFilterAndOptions:

// Determine select fields based on whether count is requested
selectFields := "transaction_id, source, reference, amount, precise_amount, precision, currency, destination, description, status, hash, created_at, effective_date, meta_data"
if opts != nil && opts.IncludeCount {
    selectFields += ", COUNT(*) OVER() AS total_count"
}

14 columns — no parent_transaction. The matching rows.Scan blocks (both the IncludeCount and the plain branch) likewise never scan it.

For contrast, GetTransaction (single fetch) does select it:

SELECT transaction_id, source, reference, amount, precise_amount, precision,
       currency, destination, description, status, created_at, meta_data,
       parent_transaction, hash
FROM blnk.transactions WHERE transaction_id = $1

The column exists and is indexed:

  • sql/1708676327.sql / sql/1714213688.sqlparent_transaction TEXT
  • sql/1721227454.sql:17CREATE INDEX idx_transactions_parent_transaction

So this is a plain omission in one hand-maintained column list, not a schema or design limitation.

Steps to reproduce

# 1. Create an inflight transaction, then commit it — the committed
#    child carries parent_transaction = the inflight id.
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":"@World","destination":"<balance_id>","description":"x",
       "inflight":true,"skip_queue":true,"allow_overdraft":true}')
INFID=$(echo "$INF" | jq -r .transaction_id)

CHILD=$(curl -s -X PUT "http://localhost:5001/transactions/inflight/$INFID" \
  -H 'X-Blnk-Key: <key>' -H 'Content-Type: application/json' \
  -d '{"status":"commit"}')
CHILDID=$(echo "$CHILD" | jq -r .transaction_id)

# 2. GET the child — parent_transaction is populated.
curl -s "http://localhost:5001/transactions/$CHILDID" -H 'X-Blnk-Key: <key>' | jq .parent_transaction
#   -> "txn_<the inflight id>"        ✅

# 3. Filter for the SAME child by parent_transaction — it matches the
#    row, yet the returned object's parent_transaction is empty.
curl -s -X POST http://localhost:5001/transactions/filter \
  -H 'X-Blnk-Key: <key>' -H 'Content-Type: application/json' \
  -d '{"filters":[{"field":"parent_transaction","operator":"eq","value":"'"$INFID"'"}]}' \
  | jq '.[0].parent_transaction'
#   -> ""                              ❌  BUG

Observed

Endpoint parent_transaction in the response
GET /transactions/{id} txn_… (correct)
POST /transactions/filter "" (empty)

Expected

Both endpoints describe the same transaction and should return the same parent_transaction value. The Filter API must return the field — it can already be filtered on it.

Suggested fix

Add parent_transaction to selectFields in GetAllTransactionsWithFilterAndOptions, and add the matching &transaction.ParentTransaction to both rows.Scan blocks (the IncludeCount branch and the plain branch). One column.

Contributor guide