Skip to content

Commit f800a9f

Browse files
authored
M9-S3: seller query endpoints + ExtendedBiddingTriggered handler carry-forward (#106)
Obligations BC: GET /api/obligations/status?sellerId= — pure IQuerySession read over the existing ObligationStatusView inline projection. Settlement BC: SellerSettlementSummary handler-driven projection from SettlementCompleted, with GET /api/settlement/summaries?sellerId= endpoint. Rides the settlement-settlement-events queue alongside PendingSettlementHandler under Separated dispatch. Listings BC: AuctionStatusHandler.Handle(ExtendedBiddingTriggered) advances CatalogListingView.ScheduledCloseAt; publish route added to listings-auctions-events in Program.cs. Cache-bridge burst-final hardening evaluated and deferred — the fix is localised but affects three SPAs and the seller cache bridge doesn't exist yet; natural landing point is the @critterbids/shared extraction (M9-S5/S7). All five seller-facing endpoint gaps from the M9 milestone audit are now closed. 10 new integration tests (326 total, 0 failures).
1 parent 7c498d1 commit f800a9f

12 files changed

Lines changed: 853 additions & 4 deletions

File tree

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# M9-S3: Backend Precursor — Seller Query Endpoints + Housekeeping
2+
3+
**Milestone:** M9 ([Seller Console](../../milestones/M9-seller-console.md))
4+
**Slice:** S3 of M9 (second backend precursor slice)
5+
**Narrative:** `docs/narratives/006-seller-fulfills-post-sale-obligation.md` (obligation fulfillment) · `docs/narratives/005-seller-watches-flash-auction-close.md` (extended bidding visibility)
6+
**Agent:** @PSA
7+
**Estimated scope:** one PR, ~12-15 files (2 query endpoints, 1 projection + handler, 1 handler addition, 1 publish route, integration tests, retro)
8+
9+
---
10+
11+
## Preconditions
12+
13+
This prompt assumes **`docs/milestones/M9-seller-console.md` exists** (authored 2026-06-13, PR #103) and that M9-S2 shipped (`7c498d1`, PR #105 — seller listing endpoints + `SellerListingSummary` projection). Per AUTHORING.md rule 3 the milestone doc is authoritative for scope. The working branch starts from clean `main` at `7c498d1`.
14+
15+
## Goal
16+
17+
Wire the two remaining seller-facing query endpoints from the M9 milestone doc endpoint surface audit (§2), ship the M8-S7 carry-forward Listings handler, and evaluate the cache-bridge burst-final hardening — completing all backend precursor work before the seller SPA slices (M9-S4+):
18+
19+
1. **Obligations BC: `GET /api/obligations/status?sellerId={sellerId}`** — query endpoint against the existing `ObligationStatusView` inline projection, filtered by seller
20+
2. **Settlement BC: `SellerSettlementSummary` + `GET /api/settlement/summaries?sellerId={sellerId}`** — new handler-driven tolerant-upsert document seeded from `SettlementCompleted`, plus query endpoint
21+
3. **Listings BC: `ExtendedBiddingTriggered` handler**`CatalogListingView.ScheduledCloseAt` advances on extension (the M8-S7 carry-forward that made the extended-bidding banner unreachable)
22+
4. **Cache-bridge burst-final hardening** — evaluate and ship or defer with rationale
23+
24+
## Context to load
25+
26+
| File | Purpose |
27+
|---|---|
28+
| `docs/milestones/M9-seller-console.md` | Authoritative for scope. §7 S3 row, §2 endpoint audit, §9 carry-forwards. |
29+
| `CLAUDE.md` | Routing layer and global conventions. |
30+
| `docs/skills/critter-stack-testing-patterns/SKILL.md` | Integration test patterns, cross-BC handler isolation. |
31+
| `docs/skills/wolverine-message-handlers/SKILL.md` | Handler and HTTP endpoint patterns. |
32+
| `docs/skills/marten-projections/SKILL.md` | Handler-driven tolerant-upsert pattern (for SellerSettlementSummary). |
33+
| `src/CritterBids.Obligations/ObligationStatusView.cs` | Existing view shape — queryable by SellerId. |
34+
| `src/CritterBids.Obligations/ObligationsModule.cs` | Module registration. |
35+
| `src/CritterBids.Selling/GetSellerListingsEndpoint.cs` | The M9-S2 query endpoint precedent (IQuerySession + seller filter). |
36+
| `src/CritterBids.Settlement/PendingSettlementHandler.cs` | The tolerant-upsert handler precedent in Settlement BC. |
37+
| `src/CritterBids.Settlement/SettlementModule.cs` | Module registration. |
38+
| `src/CritterBids.Contracts/Settlement/SettlementCompleted.cs` | Integration event carrying settlement financial fields. |
39+
| `src/CritterBids.Listings/AuctionStatusHandler.cs` | The class to extend with ExtendedBiddingTriggered handling. |
40+
| `src/CritterBids.Contracts/Auctions/ExtendedBiddingTriggered.cs` | The contract event shape. |
41+
| `src/CritterBids.Api/Program.cs` | RabbitMQ publish routing — ExtendedBiddingTriggered currently missing from `listings-auctions-events`. |
42+
| `docs/retrospectives/M8-S7-end-to-end-housekeeping-retrospective.md` | Carry-forward findings (§Finding 1: handler gap, §Finding 2: cache-bridge race). |
43+
44+
## In scope
45+
46+
### S3a: Obligations seller query endpoint
47+
48+
- New file: `src/CritterBids.Obligations/GetSellerObligationsEndpoint.cs`
49+
- `GET /api/obligations/status?sellerId={sellerId}` — Wolverine HTTP GET endpoint using `IQuerySession` to query `ObligationStatusView` by `SellerId`
50+
- Returns `IReadOnlyList<ObligationStatusView>`
51+
- `[AllowAnonymous]` — seller-facing, not staff-gated
52+
- Mirrors the `GetSellerListingsEndpoint` pattern from M9-S2
53+
54+
### S3b: Settlement seller summary projection + query endpoint
55+
56+
- New file: `src/CritterBids.Settlement/SellerSettlementSummary.cs``sealed record` capturing the settlement financial outcome per listing for sellers
57+
- Fields: `Id` (Guid, ListingId — natural key), `SettlementId`, `SellerId`, `WinnerId`, `HammerPrice`, `FeeAmount`, `SellerPayout`, `CompletedAt`
58+
- New file: `src/CritterBids.Settlement/SellerSettlementSummaryHandler.cs` — handler-driven tolerant-upsert consuming `SettlementCompleted`
59+
- Same tolerant-upsert shape as `PendingSettlementHandler`: LoadAsync by ListingId, construct if absent, store
60+
- Consumes `SettlementCompleted` from local saga dispatch (the saga self-publishes it via `OutgoingMessages`)
61+
- Sticky binding: `[StickyHandler("settlement-settlement-events")]` — rides the Settlement BC's self-consumption queue, matching `PendingSettlementHandler.Handle(SettlementCompleted)`
62+
- Register `SellerSettlementSummary` in `SettlementModule.ConfigureMarten()``settlement` schema
63+
- New file: `src/CritterBids.Settlement/GetSellerSettlementsEndpoint.cs`
64+
- `GET /api/settlement/summaries?sellerId={sellerId}` — queries `SellerSettlementSummary` by `SellerId`
65+
- Returns `IReadOnlyList<SellerSettlementSummary>`
66+
- `[AllowAnonymous]`
67+
68+
### S3c: Listings ExtendedBiddingTriggered handler
69+
70+
- Add `Handle(ExtendedBiddingTriggered)` method to the existing `AuctionStatusHandler` class
71+
- Tolerant-upsert: LoadAsync by ListingId, construct minimal view if absent
72+
- Updates `ScheduledCloseAt = message.NewCloseAt`
73+
- Same Withdrawn-preservation guard as `Handle(BiddingOpened)` — if the listing is already Withdrawn, no-op
74+
- Add publish route in `Program.cs`: `ExtendedBiddingTriggered``listings-auctions-events`
75+
- The event is already routed to `relay-auctions-events` and `auctions-auctions-events`; this adds the Listings consumer route
76+
77+
### S3d: Cache-bridge burst-final hardening (evaluation)
78+
79+
- Evaluate the delayed re-invalidation approach documented in M8-S7 Finding 2
80+
- The push-refetch race: a hub push arrives before the sibling-queue projection applies; the re-query reads stale data; the last event of a burst has no later push to reconcile
81+
- If the fix is localised (a delayed re-invalidate in the cache bridge — `setTimeout(() => invalidateQueries(...), 500)` after the immediate invalidation), ship it in both bidder and seller cache bridges
82+
- If it requires infrastructure changes or cross-cutting framework work, record rationale and defer
83+
84+
### S3e: Integration tests
85+
86+
- New file: `tests/CritterBids.Obligations.Tests/GetSellerObligationsApiTests.cs` — HTTP-level tests:
87+
- Happy path: seed obligation status views for a seller, query by sellerId, verify returned
88+
- Empty: query for unknown sellerId returns empty list
89+
- Filtering: obligations from a different seller are not returned
90+
- New file: `tests/CritterBids.Settlement.Tests/SellerSettlementSummaryTests.cs` — tests for the projection and query:
91+
- Happy path: dispatch SettlementCompleted, verify SellerSettlementSummary document created with correct fields
92+
- Query: seed summaries for a seller, query by sellerId, verify returned
93+
- Filtering: summaries from a different seller are not returned
94+
- Extend `tests/CritterBids.Listings.Tests/CatalogListingViewTests.cs` with ExtendedBiddingTriggered test:
95+
- Seed a CatalogListingView at "Open" with ScheduledCloseAt, dispatch ExtendedBiddingTriggered, verify ScheduledCloseAt advanced to NewCloseAt
96+
- Withdrawn guard: seed at "Withdrawn", dispatch ExtendedBiddingTriggered, verify ScheduledCloseAt unchanged
97+
98+
### S3f: Retrospective
99+
100+
- `docs/retrospectives/M9-S3-seller-query-endpoints-housekeeping-retrospective.md`
101+
102+
## Explicitly out of scope
103+
104+
- **Frontend changes.** No seller UI, no bidder/ops changes, no `client/` touches (unless the cache-bridge fix is localised — that's the one sanctioned frontend touch). M9-S4+ consumes these endpoints.
105+
- **New domain events.** The endpoints query existing projections. The `SellerSettlementSummary` consumes an existing integration event. No new contract types.
106+
- **Obligations endpoint for winners.** The `ObligationStatusView` has `WinnerId` but the seller query filters by `SellerId` only. Winner-side obligation queries are not M9 scope.
107+
- **Failed settlement details for sellers.** The `SellerSettlementSummary` captures completed settlements only (from `SettlementCompleted`). Failed settlement status is available via the listing's `PendingSettlement.Status == Failed`. A combined view is post-MVP.
108+
- **`docs/STATUS.md` regeneration.** Deferred to M9-S7.
109+
- **Changing existing RabbitMQ queue topology.** Only adding one new publish route for `ExtendedBiddingTriggered``listings-auctions-events`.
110+
- **Extended-bidding e2e banner assert.** The M8-S7 e2e marks the spot for the banner assert to be re-added; that's an M9-S7 follow-up after the seller console's frontend renders it.
111+
112+
## Conventions to pin or follow
113+
114+
- **Query endpoint pattern:** `GetSellerListingsEndpoint` from M9-S2 is the precedent — `IQuerySession` + `.Where(x => x.SellerId == sellerId)` + `IReadOnlyList<T>` return.
115+
- **Handler-driven tolerant-upsert:** The `PendingSettlementHandler` shape — LoadAsync, construct if absent, mutate via `with`, session.Store. Per `marten-projections.md` §"Handler-Driven Projections — Tolerant Upsert".
116+
- **Sticky handler bindings:** Match the BC's existing queue topology. Obligations query endpoint has no handler (pure query). Settlement handler rides `settlement-settlement-events`.
117+
- **Listings handler extension:** New method on `AuctionStatusHandler`, matching the existing 6-method pattern. Class-level `[StickyHandler("listings-auctions-events")]` covers the new method.
118+
- **`[AllowAnonymous]` on seller-facing endpoints:** per CLAUDE.md and ADR-024.
119+
- **`sealed record` for new types.**
120+
- **Cross-BC handler isolation in tests:** each BC test fixture's exclusion set may need extension. Check whether new handlers or events cause discovery conflicts.
121+
122+
## Spec delta
123+
124+
Per ADR 020: this slice has **no spec consequence** on narratives or workshops. It exposes existing read models over HTTP and adds a Settlement-side projection from an existing integration event. The Listings handler addition is a carry-forward fix, not a new narrative Moment. The spec consequence is limited to the endpoint surface audit in the milestone doc (§2): the final two gaps (obligation status, settlement summary) are closed by this slice, and the M8-S7 carry-forward (Listings `ExtendedBiddingTriggered` handler) is resolved.
125+
126+
## Acceptance criteria
127+
128+
- [ ] `GET /api/obligations/status?sellerId={sellerId}` returns the seller's obligation status views
129+
- [ ] `SellerSettlementSummary` exists as a handler-driven document in the Settlement BC, registered in `SettlementModule.ConfigureMarten()`
130+
- [ ] `GET /api/settlement/summaries?sellerId={sellerId}` returns the seller's settlement summaries
131+
- [ ] `AuctionStatusHandler.Handle(ExtendedBiddingTriggered)` advances `CatalogListingView.ScheduledCloseAt`
132+
- [ ] `ExtendedBiddingTriggered` publish route added to `listings-auctions-events` in Program.cs
133+
- [ ] Cache-bridge burst-final hardening evaluated and either shipped or deferred with rationale
134+
- [ ] Integration tests cover happy path, empty, and filtering for both query endpoints
135+
- [ ] Integration tests cover ExtendedBiddingTriggered handler (happy path + Withdrawn guard)
136+
- [ ] Existing .NET build succeeds: 0 errors, 2 CS0108 warnings (baseline held)
137+
- [ ] Existing .NET tests pass: 316 baseline preserved or grown
138+
- [ ] No new domain events, no new integration events, no new BC modules
139+
- [ ] No frontend changes (unless the cache-bridge fix is localised)
140+
- [ ] `docs/retrospectives/M9-S3-seller-query-endpoints-housekeeping-retrospective.md` written with `**Prompt:**` header and `## Spec delta -- landed?` paragraph
141+
- [ ] No commit to `main`; one PR off `main`; no `Co-Authored-By` trailer
142+
143+
## Open questions
144+
145+
- **SellerSettlementSummary sticky queue:** The handler consumes `SettlementCompleted` which `PendingSettlementHandler` already handles on `settlement-settlement-events`. With `MultipleHandlerBehavior.Separated`, two handlers for the same message type on the same queue should each get their own chain (per ADR 027). Verify this works in the test fixture.
146+
- **Obligations query: Marten schema for IQuerySession:** The `ObligationStatusView` is registered in `ObligationsModule.ConfigureMarten()` with schema `obligations`. The query endpoint uses `IQuerySession` which reads from the same Marten store. No additional registration needed — but verify the query endpoint test works with the existing fixture.

0 commit comments

Comments
 (0)