---
title: Converge the auction sale payload and give the transactions slice its case
type: task
status: completed
description: >-
  The two AUCTION_PROCESSED writers disagree in shape and the client transactions slice has no case
  for the sale, so an auction sale reaches league-recent-transactions only after a reload. Converge
  both writers on the transaction row plus pos.
base_uri: user:task/league/converge-the-auction-sale-payload.md
created_at: '2026-09-06T19:38:07.018Z'
entity_id: 432ec322-9156-4c36-ac95-f4a714d45555
finished_at: '2026-09-06T19:55:42.688Z'
observations:
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: Low
public_read: true
tags:
  - user:tag/league-xo-football.md
updated_at: '2026-09-06T20:05:48.601Z'
visibility_analyzed_at: '2026-11-05T23:48:38.166Z'
---

## Problem

The two `AUCTION_PROCESSED` writers disagree in shape, and the client transactions slice has no case for the sale at all.

- `broadcast_auction_settlement` (election mode — every 2026 settlement) sends four fields: `{ pid, tid, player_salary, pos }`.
- The socket's `_announce_auction_transaction` (live blocks) sends a whole `transactions` row plus `rid: 0`.
- `app/core/transactions/reducer.js` has no `AUCTION_PROCESSED` case, so `league-recent-transactions` on the league-home page shows an auction sale only after a reload.

Low severity on its own — the sale is off the auction page, whose own component reads the live `auction.transactions` slice. It is worth doing because the fix forces the payload's contract, which the two writers currently leave undefined.

## Clean end state

ONE payload shape from both writers: the inserted transaction row plus `pos`.

- `persist_auction_settlement` already inserts the `AUCTION_PROCESSED` row. Have it `.returning('transaction_id')` and hand the row back, the way the socket path already does in `_record_auction_transaction`.
- `settle_auction_player_if_complete` carries it on `settlement`; `broadcast_auction_settlement` sends `{ ...transaction, pos }`.
- Drop the socket's `rid: 0`. It is vestigial: the rosters reducer's `AUCTION_PROCESSED` case destructures only `{ tid, pid, pos, player_salary }` and pushes an object with no `rid`. Verify that before removing it.

## The gate is already built, which is why this is cheap

`test/utils/auction-processed-client-fields.mjs` derives the demanded field set from the reducers themselves. Adding an `AUCTION_PROCESSED` case to the transactions reducer that reads `transaction_id` and `occurred_at` makes both `test/auction.election-broadcast.spec.mjs` and `test/auction.settlement-client-state.spec.mjs` demand those fields of their writer automatically. So the convergence is enforced by the existing gates rather than by a new hand-written assertion — write the reducer case FIRST and watch the election-broadcast spec go red, which is also the proof the gate discriminates.

`test/auction.settlement-client-state.spec.mjs` pins the list of reducer files that handle the sale; adding a third means updating that list deliberately.

## Plan

- [x] Confirm `rid` is dead on this payload, then converge both writers on `{ ...transaction, pos }`
- [x] Add the `AUCTION_PROCESSED` case to `app/core/transactions/reducer.js`, prepending to `items` — newest-first, since `GET_TRANSACTIONS_FULFILLED` appends older pages
- [x] **Guard the prepend on the active filter.** `items` is server-filtered on the transactions page, so an unguarded prepend injects a row violating the filter the user set. **This claim was measured WRONG.** `state.types` is fully populated by default, but `state.teams` initializes to an EMPTY List and is filled only by `GET_TEAMS_FULFILLED` — and the route applies `whereIn` only `if (teams.length)`, so an empty list means UNFILTERED rather than nothing. A bare `includes` therefore drops the row on the default page. The shipped guard escapes the empty set on both terms.
- [x] Score it by deleting the reducer case and by dropping a converged field from each writer separately, requiring each mutation to redden a different spec
- [x] Update `docs/guides/auction.md` § "The sale payload is the whole of what the client learns", which currently documents the four-field election shape

## Verification

Run `scripts/drive-auction-end-to-end.mjs --lid 119` after the change — it drives both writers against the production database and settled a real player on 2026-09-06. The guide's `--only settlement` flag scopes it to about 13 seconds.

## Cross-references

- [[user:task/league/retire-write-only-team-salary-cap.md]] — completed 2026-09-06; surfaced this defect and removed the teams reducer's own case from the payload's demand set.
- `docs/guides/auction.md` § "The sale payload is the whole of what the client learns" — the two-writer rule and why a missing field is an `undefined` in client state rather than a stale value.
