diff --git a/src/app/api/slack/interactive/route.ts b/src/app/api/slack/interactive/route.ts index 08f0689..2286bbe 100644 --- a/src/app/api/slack/interactive/route.ts +++ b/src/app/api/slack/interactive/route.ts @@ -234,6 +234,16 @@ async function buildDetailsModal(transactionId: number, participantId: number) { `SELECT merchant_normalized AS merchant, line_items FROM expense_metadata WHERE transaction_id = $1 OR matched_transaction_id = $1 +-- Ordered the same way as replaceLineItems (line-items.ts) and + -- matchTransaction (intake-router.ts), so every reader of this row + -- resolves the same one a write lands on. NULLS LAST because DESC is + -- NULLS FIRST and the row that merely POINTS at the transaction has a + -- NULL transaction_id. + -- Measured 2026-08-21: 667 expense_metadata rows, 147 of them + -- pointing-only, and ZERO transactions resolving to more than one row by + -- either link — so this changes nothing today. It is here so the readers + -- still agree on the day multiplicity appears. + ORDER BY (transaction_id = $1) DESC NULLS LAST, id LIMIT 1`, [transactionId] ); diff --git a/src/lib/order-reviews.ts b/src/lib/order-reviews.ts index 05d434e..46739e7 100644 --- a/src/lib/order-reviews.ts +++ b/src/lib/order-reviews.ts @@ -119,6 +119,16 @@ export async function merchantForTransaction( const row = await queryRow<{ merchant_normalized: string | null }>( `SELECT merchant_normalized FROM expense_metadata WHERE transaction_id = $1 OR matched_transaction_id = $1 +-- Ordered the same way as replaceLineItems (line-items.ts) and + -- matchTransaction (intake-router.ts), so every reader of this row + -- resolves the same one a write lands on. NULLS LAST because DESC is + -- NULLS FIRST and the row that merely POINTS at the transaction has a + -- NULL transaction_id. + -- Measured 2026-08-21: 667 expense_metadata rows, 147 of them + -- pointing-only, and ZERO transactions resolving to more than one row by + -- either link — so this changes nothing today. It is here so the readers + -- still agree on the day multiplicity appears. + ORDER BY (transaction_id = $1) DESC NULLS LAST, id LIMIT 1`, [transactionId] ); diff --git a/src/lib/queries.ts b/src/lib/queries.ts index 6fffce4..b18b0c2 100644 --- a/src/lib/queries.ts +++ b/src/lib/queries.ts @@ -367,6 +367,16 @@ export async function getTransactions(ownerId: number, filters: TransactionFilte SELECT em.route, em.platform FROM expense_metadata em WHERE em.transaction_id = t.id OR em.matched_transaction_id = t.id + -- Ordered the same way as replaceLineItems (line-items.ts) and + -- matchTransaction (intake-router.ts), so every reader of this row + -- resolves the same one a write lands on. NULLS LAST because DESC is + -- NULLS FIRST and the row that merely POINTS at the transaction has a + -- NULL transaction_id. + -- Measured 2026-08-21: 667 expense_metadata rows, 147 of them + -- pointing-only, and ZERO transactions resolving to more than one row by + -- either link — so this changes nothing today. It is here so the readers + -- still agree on the day multiplicity appears. + ORDER BY (em.transaction_id = t.id) DESC NULLS LAST, em.id LIMIT 1 ) order_ctx ON true LEFT JOIN LATERAL (