blnkfinance/blnk

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

クローズ

#292 opened on 2026/05/18

 (1 件のコメント) (1 件のリアクション) (0 人の担当者)Go (129 件のフォーク)auto 404
buggood first issue

Repository metrics

Stars
 (475 個のスター)
PR merge metrics
 (平均マージ 4d 8h) (30d で 14 merged PRs)

説明

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.

コントリビューターガイド