|
| 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