Skip to content

Refactor/simplicity drop repos integration tests#30

Merged
emeraldleaf merged 16 commits into
mainfrom
refactor/simplicity-drop-repos-integration-tests
May 26, 2026
Merged

Refactor/simplicity drop repos integration tests#30
emeraldleaf merged 16 commits into
mainfrom
refactor/simplicity-drop-repos-integration-tests

Conversation

@emeraldleaf

@emeraldleaf emeraldleaf commented May 25, 2026

Copy link
Copy Markdown
Owner

What changed

How it was built

Summary by CodeRabbit

  • Refactor
    • Simplified services: removed repository wrappers; handlers now operate directly on DbContexts with inline read projections, explicit SaveChanges, tightened idempotency and race handling, and revised recovery transaction behavior.
  • Documentation
    • Clarified architecture, handler discovery vs DI registration trap, CatalogService carve‑out, and updated service walkthroughs/status.
  • Tests
    • Moved handler correctness toward integration tests; many unit handler tests removed and testing guidance tightened.
  • Chores
    • Added Codecov config and made integration test teardown more resilient.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9f943460-891a-48bc-a18e-54eaabf3d441

📥 Commits

Reviewing files that changed from the base of the PR and between 568c3c3 and ee9458b.

📒 Files selected for processing (2)
  • OrderService/Features/PlaceOrder.cs
  • docs/code-flows/shippingservice.md

Walkthrough

Removes EF repository wrapper interfaces/implementations and updates handlers, recovery jobs, DI, docs, and tests to use DbContext directly: inline AsNoTracking projections for reads, tracked load/mutate + SaveChanges for writes, DI registration updates, and tightened reviewer/testing guidance.

Changes

Simplicity Refactor: Repository-Wrapper Removal

Layer / File(s) Summary
Architectural rules and reviewer guidance
.claude/agents/architecture-reviewer.md, .coderabbit.yaml, CLAUDE.md, docs/how-it-works.md
Reviewer rules and config updated to require DbContext-based handler patterns: inline AsNoTracking().Select(...) projections for reads, tracked aggregate load/mutate + SaveChangesAsync for writes, integration-test-first guidance, CatalogService carve-out, and DI-registration rule for handlers resolved in tests.
OrderService handler refactor
OrderService/Features/*.cs, OrderService/Infrastructure/DependencyInjection.cs, tests/OrderService.Tests.Integration/*
Handlers now inject OrderDbContext instead of IOrderRepository; read handlers use AsNoTracking + projection, write handlers load tracked aggregates and call SaveChangesAsync; IOrderRepository and OrderRepository removed and DI updated; integration read tests updated to call handlers.
PaymentService: ProcessPayment & Recovery job
PaymentService/Features/ProcessPayment.cs, PaymentService/Infrastructure/PaymentRecoveryJob.cs, PaymentService/Infrastructure/DependencyInjection.cs
ProcessPaymentHandler uses PaymentDbContext with idempotency query and DbUpdateException race handling; PaymentRecoveryJob inlines explicit EF transactions (Begin → SaveChanges → Commit) for outbox atomicity and processes per-id with fresh scopes; IPaymentRepository and its adapter removed.
ShippingService handlers and DI
ShippingService/Features/*.cs, ShippingService/Infrastructure/DependencyInjection.cs
Shipment handlers switched to ShippingDbContext for idempotency, inline read projections, and persistence; IShipmentRepository/ShipmentRepository removed and DI registration dropped.
Docs, tests, and CI
docs/*, README.md, tests/**/OrderReadProjectionTests.cs, codecov.yml
Code-flow docs updated to show DbContext patterns; STATUS and README document the refactor and CatalogService carve-out; many unit tests that mocked repositories were removed and integration tests updated to invoke handlers; guidance added on handler DI registration for tests; OrderApiFactory Dispose made shutdown resilient; Codecov config added.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main refactoring initiative: removing repository wrappers and integrating tests, aligning with the changeset's primary objective of shifting from mocked-repo unit tests to integration tests with DbContext direct usage.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/simplicity-drop-repos-integration-tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented May 25, 2026

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ShippingService/Features/CreateShipment.cs (1)

38-53: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard the idempotency race on unique OrderId.

Two concurrent deliveries can both pass FirstOrDefaultAsync and one will fail on SaveChangesAsync. Treat that conflict as “already created” and return the existing shipment ID.

Also applies to: 66-67

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ShippingService/Features/CreateShipment.cs` around lines 38 - 53, Wrap the
SaveChangesAsync in the CreateShipment flow (the handler that calls
context.Shipments.FirstOrDefaultAsync, Shipment.Create and shipment.Dispatch,
and then context.Shipments.AddAsync) with a try/catch that treats a
unique-OrderId conflict as success: on failure query
context.Shipments.FirstOrDefaultAsync(s => s.OrderId == request.OrderId) and if
found return that existing.Id, otherwise rethrow the exception; apply the same
pattern to the other create path referenced around the code lines for the
duplicate check (the other location calling FirstOrDefaultAsync and
SaveChangesAsync at 66-67).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CLAUDE.md`:
- Around line 18-28: Add the mandated PR-note string to the CLAUDE.md change:
insert the exact sentence "Run /check-rules locally to audit paraphrases against
this diff." into the PR context for this file (e.g., appended to the CLAUDE.md
change block or as a PR comment associated with the CLAUDE.md edit) so the
rule-paraphrase audit is flagged; ensure the sentence is verbatim and clearly
visible in the PR where CLAUDE.md is modified.
- Around line 27-28: The file contains a duplicated bullet titled "Exception:
outbox-atomic non-handler code." — remove the redundant duplicate so only a
single instance of that bullet remains; search for the exact bullet text
("Exception: outbox-atomic non-handler code." and its following sentence about
BeginTransactionAsync → work → SaveChangesAsync → CommitAsync and
IPaymentRepository.ExecuteInTransactionAsync) and delete the second copy,
keeping the first intact to avoid drift.

In `@PaymentService/Features/ProcessPayment.cs`:
- Around line 52-63: The pre-check + insert can race and cause a
unique-constraint DbUpdateException on SaveChangesAsync; update the handler that
uses context.Payments, Payment.Create and SaveChangesAsync to catch the
unique-index violation (DbUpdateException/SqlException depending on provider)
around the AddAsync/SaveChangesAsync call, and in that catch query the existing
payment by OrderId (e.g., context.Payments.FirstOrDefaultAsync(p => p.OrderId ==
request.OrderId)) and return its Id; ensure other exceptions are rethrown so
only uniqueness conflicts are handled as idempotent returns.

In `@PaymentService/Infrastructure/PaymentRecoveryJob.cs`:
- Around line 115-117: The loop that handles recovered payments currently
creates a single DI scope via scopeFactory.CreateScope() and reuses
PaymentDbContext and IEventPublisher across all rows; change this so the
scopeFactory.CreateScope(), retrieving PaymentDbContext and IEventPublisher,
happens inside the per-row loop and wrap the processing of each row in its own
try/catch so one failure doesn't stop the batch; ensure each per-row scope is
disposed after use (so the change tracker is not reused) and consider using
AsNoTracking() for read queries if appropriate; update the code paths in
PaymentRecoveryJob that reference scopeFactory.CreateScope(), PaymentDbContext,
and IEventPublisher accordingly.

---

Outside diff comments:
In `@ShippingService/Features/CreateShipment.cs`:
- Around line 38-53: Wrap the SaveChangesAsync in the CreateShipment flow (the
handler that calls context.Shipments.FirstOrDefaultAsync, Shipment.Create and
shipment.Dispatch, and then context.Shipments.AddAsync) with a try/catch that
treats a unique-OrderId conflict as success: on failure query
context.Shipments.FirstOrDefaultAsync(s => s.OrderId == request.OrderId) and if
found return that existing.Id, otherwise rethrow the exception; apply the same
pattern to the other create path referenced around the code lines for the
duplicate check (the other location calling FirstOrDefaultAsync and
SaveChangesAsync at 66-67).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 33cfaf10-646c-4c36-a4c2-b8dc33e76eca

📥 Commits

Reviewing files that changed from the base of the PR and between e89ad72 and 4d67057.

📒 Files selected for processing (30)
  • .claude/agents/architecture-reviewer.md
  • .coderabbit.yaml
  • CLAUDE.md
  • OrderService/Domain/IOrderRepository.cs
  • OrderService/Infrastructure/OrderRepository.cs
  • PaymentService/Domain/IPaymentRepository.cs
  • PaymentService/Features/ProcessPayment.cs
  • PaymentService/Infrastructure/DependencyInjection.cs
  • PaymentService/Infrastructure/PaymentRecoveryJob.cs
  • PaymentService/Infrastructure/PaymentRepository.cs
  • README.md
  • ShippingService/Domain/IShipmentRepository.cs
  • ShippingService/Features/CreateShipment.cs
  • ShippingService/Features/GetShipmentByOrder.cs
  • ShippingService/Infrastructure/DependencyInjection.cs
  • ShippingService/Infrastructure/ShipmentRepository.cs
  • docs/STATUS.md
  • docs/code-flows/orderservice.md
  • docs/code-flows/paymentservice.md
  • docs/code-flows/shippingservice.md
  • tests/OrderService.Tests.Unit/Application/GetOrderByIdHandlerTests.cs
  • tests/OrderService.Tests.Unit/Application/GetOrdersByBuyerHandlerTests.cs
  • tests/OrderService.Tests.Unit/Application/PaymentCompletedHandlerTests.cs
  • tests/OrderService.Tests.Unit/Application/PaymentFailedHandlerTests.cs
  • tests/OrderService.Tests.Unit/Application/PlaceOrderHandlerTests.cs
  • tests/OrderService.Tests.Unit/Application/ShipmentDispatchedHandlerTests.cs
  • tests/PaymentService.Tests.Unit/Application/PaymentRecoveryJobTests.cs
  • tests/PaymentService.Tests.Unit/Application/ProcessPaymentHandlerTests.cs
  • tests/ShippingService.Tests.Unit/Application/CreateShipmentHandlerTests.cs
  • tests/ShippingService.Tests.Unit/Application/GetShipmentByOrderHandlerTests.cs
💤 Files with no reviewable changes (16)
  • tests/OrderService.Tests.Unit/Application/PaymentFailedHandlerTests.cs
  • tests/PaymentService.Tests.Unit/Application/ProcessPaymentHandlerTests.cs
  • tests/OrderService.Tests.Unit/Application/GetOrdersByBuyerHandlerTests.cs
  • tests/ShippingService.Tests.Unit/Application/GetShipmentByOrderHandlerTests.cs
  • tests/OrderService.Tests.Unit/Application/PaymentCompletedHandlerTests.cs
  • ShippingService/Infrastructure/ShipmentRepository.cs
  • PaymentService/Domain/IPaymentRepository.cs
  • tests/OrderService.Tests.Unit/Application/GetOrderByIdHandlerTests.cs
  • tests/PaymentService.Tests.Unit/Application/PaymentRecoveryJobTests.cs
  • tests/OrderService.Tests.Unit/Application/ShipmentDispatchedHandlerTests.cs
  • OrderService/Infrastructure/OrderRepository.cs
  • OrderService/Domain/IOrderRepository.cs
  • tests/ShippingService.Tests.Unit/Application/CreateShipmentHandlerTests.cs
  • ShippingService/Domain/IShipmentRepository.cs
  • PaymentService/Infrastructure/PaymentRepository.cs
  • tests/OrderService.Tests.Unit/Application/PlaceOrderHandlerTests.cs

Comment thread CLAUDE.md Outdated
Comment thread CLAUDE.md Outdated
Comment thread PaymentService/Features/ProcessPayment.cs
Comment thread PaymentService/Infrastructure/PaymentRecoveryJob.cs Outdated
…s as default for handlers

Inverts two project rules that were reinforcing each other into a
suboptimal local minimum:

- CLAUDE.md "Interfaces earn their keep": removes IFooRepository
  from the legitimate-port list. Adds new section "Data access:
  DbContext directly, no repository wrappers" explaining the
  reasoning (DbContext IS Unit-of-Work, DbSet<T> IS Repository,
  wrapping adds layers without capability) and listing the
  legitimate exceptions (IEventPublisher, IPaymentGateway,
  ICatalogClient, INotificationSender, IProductCache,
  IProductReadStore).

- .coderabbit.yaml: new path_instructions for
    **/Domain/I*Repository.cs (flag re-introduction)
    **/Features/**/*.cs (handler takes DbContext direct)
    **/*Test*.cs (integration tests default; unit only for pure domain)
    CatalogService/CatalogService.Application/Interfaces/IProductReadStore.cs
        (the one read-side port that survives — Clean layer-rule
        consequence, not wrapper-around-EF)

- architecture-reviewer.md pattern checklist:
    Removed the "When reviewing repositories" section
    Added "When reviewing write handlers" (DbContext direct,
        outbox-atomic non-handler code preserved inline)
    Updated "When reviewing query handlers" (DbContext direct +
        CatalogService Clean exception)
    Updated "When reviewing Domain folders" (flag I*Repository
        re-introduction)
    Updated "When reviewing tests" (integration default for
        handler tests; unit only for pure domain; legitimate
        Substitute.For<T> targets enumerated)

No code changes yet — this commit just establishes the new canonical
rules so subsequent service refactors have a single target. The actual
code refactor (delete I*Repository files, refactor handlers to use
DbContext directly, move handler tests from unit to integration project)
follows in service-by-service commits.

Motivation: this is a learning project shown to a team that emphasizes
simplicity, and the I*Repository wrappers were the project's most
visible deviation from "DbContext IS Unit-of-Work, DbSet<T> IS
Repository." They were also the local minimum holding the
unit-test-with-mocks pattern in place. Pulling on either thread
without the other left an awkward shape; this commit closes the loop
on the rules so the code refactor can follow cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@emeraldleaf emeraldleaf force-pushed the refactor/simplicity-drop-repos-integration-tests branch from 4d67057 to fc17711 Compare May 25, 2026 23:35
Comment thread PaymentService/Infrastructure/PaymentRecoveryJob.cs Fixed
emeraldleaf and others added 7 commits May 25, 2026 17:40
…ontext directly

Per the new rule in CLAUDE.md "Data access: DbContext directly, no repository wrappers":
DbContext IS Unit-of-Work and DbSet<T> IS Repository. The IOrderRepository wrapper
added layers without capability — the only reason for its existence was to enable
mocking in unit tests, which we've replaced with integration tests against
Testcontainers (real SQL Server via OrderApiFactory).

Code changes:
- DELETED: OrderService/Domain/IOrderRepository.cs
- DELETED: OrderService/Infrastructure/OrderRepository.cs
- Removed AddScoped<IOrderRepository, OrderRepository>() from DependencyInjection.cs
- 6 handlers refactored to take OrderDbContext directly:
  * PlaceOrderHandler: context.Orders.AddAsync(...) + context.SaveChangesAsync(...)
    AutoApplyTransactions wraps SaveChanges + Wolverine outbox staging
  * GetOrderByIdHandler: inline AsNoTracking + .Select projection to OrderSummaryDto
  * GetOrdersByBuyerHandler: same shape, with the pagination clamp + long-arithmetic
    Skip-offset guard from PR #29 preserved inline
  * PaymentCompletedHandler / PaymentFailedHandler / ShipmentDispatchedHandler:
    context.Orders.FirstOrDefaultAsync(...) (tracked) → state-transition method →
    context.SaveChangesAsync(...) — same idempotency layering (handler status
    pre-check + aggregate invariant throw + RowVersion retry) as before

Test changes:
- DELETED 6 mocked-repo unit test files (23 tests total):
    GetOrderByIdHandlerTests, GetOrdersByBuyerHandlerTests,
    PaymentCompletedHandlerTests, PaymentFailedHandlerTests,
    PlaceOrderHandlerTests, ShipmentDispatchedHandlerTests
  Coverage of these handler paths is already provided by the existing
  integration suite (OrderSagaTests covers PlaceOrder + PaymentCompleted +
  idempotency + RowVersion; OrderReadProjectionTests covers both read handlers).
- KEPT all legitimate unit tests: PlaceOrderCommandValidator, OrderTests (aggregate
  state guards), OrderLineTests, ContextPropagationMiddleware, CorrelationIdMiddleware,
  OutgoingContextMiddleware, GlobalExceptionHandler, ServiceDefaultsJwt
- OrderReadProjectionTests refactored to resolve GetOrderByIdHandler /
  GetOrdersByBuyerHandler from DI instead of the deleted IOrderRepository

Build: clean, 0 warnings, 0 errors. Unit tests: 142/142 pass (down 23 from 165
because we deleted 23 mocked-repo handler tests — net coverage unchanged since
integration suite already covers those paths).

Gaps tracked for follow-up:
- PaymentFailedHandler and ShipmentDispatchedHandler don't have integration test
  coverage today (OrderSagaTests has PaymentCompletedHandler but not the others).
  Same pattern, ~30 lines each. Filed under "tests we owe" in STATUS.md.
- Walkthrough docs/code-flows/orderservice.md still describes IOrderRepository —
  separate commit on this branch will update it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Updates docs/code-flows/orderservice.md to reflect the refactor in b34493c:

- Phase 1 sequence diagram: Repo participant -> Ctx (OrderDbContext).
  Calls show context.Orders.AddAsync + context.SaveChangesAsync explicitly
  so the AutoApplyTransactions wrap-point is visible.
- Phase 2 sequence diagram: same lane swap. Load via
  context.Orders.FirstOrDefaultAsync; save via context.SaveChangesAsync.
- "Read-path coexistence" section renamed to "Read/write split (CQRS
  data-access pattern)". The graph LR was rebuilt to show the actual
  shape: no IOrderRepository node; just OrderDbContext with write
  handlers and read handlers flowing through different code shapes
  (load-mutate-save vs AsNoTracking + Select projection). Discipline
  lives at PR review + CodeRabbit + architecture-reviewer.
- File inventory: dropped Domain/IOrderRepository.cs and
  Infrastructure/OrderRepository.cs rows. Updated GetOrderById/
  GetOrdersByBuyer descriptions to say "projects to DTO inline".

Open questions and state machine sections were left unchanged because
the handler-level idempotency + aggregate-level invariant + RowVersion
retry layered defense is conceptually unchanged by the refactor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ippingDbContext directly

Same pattern as the OrderService refactor (b34493c):

Code changes:
- DELETED Domain/IShipmentRepository.cs and Infrastructure/ShipmentRepository.cs
- Removed AddScoped<IShipmentRepository, ShipmentRepository>() from
  Infrastructure/DependencyInjection.cs
- CreateShipmentHandler: takes ShippingDbContext directly. Existence
  check via context.Shipments.FirstOrDefaultAsync; AddAsync on the
  DbSet; explicit SaveChangesAsync after PublishAsync so Wolverine's
  AutoApplyTransactions wraps both writes + the staged
  ShipmentDispatchedEvent envelope in one DB tx.
- GetShipmentByOrderHandler: takes ShippingDbContext directly. Inline
  AsNoTracking + .Where + .Select projection to ShipmentDto.
  IDOR ownership check happens on the projected DTO's BuyerId field —
  same null → 404 semantics, same code shape, no repo seam.

Test changes:
- DELETED 2 mocked-repo unit test files:
    CreateShipmentHandlerTests.cs, GetShipmentByOrderHandlerTests.cs
- Remaining unit tests (Domain/ShipmentTests, PaymentCompletedHandler
  test which doesn't touch a repo) continue to pass.

Coverage gap: ShippingService has no integration test project today,
so the IDOR + idempotency paths the deleted unit tests covered are
currently uncovered. This was already a known gap pre-refactor (see
STATUS.md "ShippingService has no integration test project"); the
refactor makes it temporarily worse. Standing up
ShippingService.Tests.Integration (Postgres Testcontainer +
ShippingApiFactory + TestAuthHandler) is tracked separately.

Walkthrough docs/code-flows/shippingservice.md:
- Both sequence diagrams swapped Repo lane → Ctx (ShippingDbContext)
- "Read/write data-access split" section collapsed — the verbose
  graph LR no longer fits because there's no interface to diagram.
  Cross-references OrderService for the canonical explanation.
- File inventory dropped the deleted repo rows.

Build clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ic wrap in RecoveryJob

Same pattern as OrderService (b34493c) and ShippingService (9a02bda),
with one wrinkle: the ExecuteInTransactionAsync wrapper on the old
IPaymentRepository was the outbox-atomic-non-handler wrapper that
PaymentRecoveryJob depends on. The wrapper interface is gone; the
pattern (BeginTransactionAsync → entity work + PublishAsync →
SaveChangesAsync → CommitAsync) is inlined into
PaymentRecoveryJob.RecoverOneAsync, unchanged semantics.

Code changes:
- DELETED Domain/IPaymentRepository.cs and Infrastructure/PaymentRepository.cs
  (the latter contained ExecuteInTransactionAsync + GetStalePendingPaymentIdsAsync
  in addition to the standard repo methods — all migrated below)
- Removed AddScoped<IPaymentRepository, PaymentRepository>() from DI;
  added a comment pointing future readers to where the outbox-atomic
  pattern now lives (PaymentRecoveryJob)
- ProcessPaymentHandler: takes PaymentDbContext directly. Idempotency
  check via context.Payments.FirstOrDefaultAsync(p => p.OrderId == ...);
  AddAsync on the DbSet; explicit SaveChangesAsync after Pending insert,
  then again after MarkAsCompleted/MarkAsFailed + outcome event publish.
  AutoApplyTransactions wraps the final SaveChanges + outbox staging
  in one DB tx.
- PaymentRecoveryJob.SweepAsync: resolves PaymentDbContext from the
  per-iteration scope. Stale-ID query inlined (AsNoTracking + Where +
  Select(p => p.Id) + ToListAsync). RecoverOneAsync inlines the explicit
  transaction wrap that used to live behind ExecuteInTransactionAsync —
  BeginTransactionAsync → MarkAsFailed → PublishAsync → SaveChangesAsync
  → CommitAsync. Comment block in code explains that the
  BackgroundService runs OUTSIDE Wolverine's handler pipeline so
  AutoApplyTransactions does NOT fire, hence the manual wrap.

Test changes:
- DELETED 2 mocked-repo unit test files:
    ProcessPaymentHandlerTests.cs, PaymentRecoveryJobTests.cs
- Remaining unit tests (OrderPlacedHandlerTests, GlobalExceptionHandler,
  PlaceOrderCommandValidator etc.) pass.

Coverage gap: PaymentService has no integration test project today.
The same gap exists for ShippingService (post-refactor 9a02bda) — both
will need ServiceName.Tests.Integration projects to restore the
handler-level coverage that the deleted unit tests provided. Tracked
under "Open issues" in STATUS.md once the final pass lands.

Walkthrough docs/code-flows/paymentservice.md:
- Flow 1 + Flow 3 sequence diagrams: Repo lane swapped to Ctx
  (PaymentDbContext). The recovery job diagram explicitly shows the
  BeginTransactionAsync → SaveChangesAsync → CommitAsync sequence so
  the outbox-atomic-non-handler pattern is visible.
- Outbox-outside-handler trap callout: code snippet shows the inline
  pattern (no wrapper method); cross-references the recovery job.
- File inventory dropped IPaymentRepository.cs and PaymentRepository.cs.

Build clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…per rule

Earlier rule encoding (7d995db) said "delete IProductRepository from
CatalogService too." Walking that back — turns out the Clean Architecture
layer constraint makes it impossible without substantial restructure:

- CatalogService has a 4-project split: Domain / Application /
  Infrastructure / Api.
- CatalogDbContext lives in Infrastructure.
- Infrastructure already references Application (for IProductCache,
  IProductReadStore).
- Adding "Application → Infrastructure" so handlers can take CatalogDbContext
  directly would create a circular project reference.

The only "clean" path is to move CatalogDbContext to Application, which
touches 9 files + risks the migration snapshot losing track of applied
migrations. That's a separate, bigger refactor than the simplicity pass.

For this refactor: CatalogService keeps both repositories because they're
load-bearing layer-rule artifacts, not speculative coupling:
- IProductRepository (Domain) — write-side port; Application needs it
  because it can't reference Infrastructure.
- IProductReadStore (Application) — read-side port; Domain can't reference
  Contracts where DTOs live, so the DTO-returning interface lives in
  Application.

Both pass the consumer-substitution test through Clean Architecture's
project-reference structure. The 4 VSA services drop the wrappers because
they have no such layer constraint.

The diff between the patterns — wrapper kept where the layer rule needs
it, wrapper dropped where it doesn't — IS the project's intentional
calibration story. A simplicity-emphasizing reviewer should read this as
"they understand when each pattern earns its keep," not as
"inconsistency."

Rule updates:
- CLAUDE.md "Interfaces earn their keep" gets the explicit CatalogService
  exception, with the reasoning.
- .coderabbit.yaml CatalogService path_instruction is reframed as a
  carve-out: flag NEW repository wrappers, but don't flag the existing
  IProductRepository/IProductReadStore.
- architecture-reviewer.md "When reviewing query handlers" / "write
  handlers" / "Domain folders" sections all carry the carve-out so the
  agent doesn't flag CatalogService's existing pattern in future reviews.

No code changes in this commit — the encoding fix is what matters; the
CatalogService code stays as-is.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…SA services)

Adds an entry to "Recently landed" capturing:
- Motivation (DbContext IS Unit-of-Work / DbSet<T> IS Repository;
  simplicity-team reviewer optics on a learning project)
- What changed across each service (rules → OrderService → ShippingService
  → PaymentService → NotificationService → CatalogService carve-out)
- The PaymentRecoveryJob outbox-atomic wrap that got inlined
- The CatalogService Clean-Architecture carve-out and why it's load-bearing
- Test coverage: 23 mocked-repo handler tests deleted (existing integration
  suite covers most; remaining gap acknowledged for Shipping + Payment
  integration projects)
- Walkthrough updates

Updates "Last updated" timestamp to reflect this change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nce state)

Adds a lead-bullet to the "About this repo" section explaining that two
architectural eras live in the git history:

- main: post-simplicity-refactor (handlers take DbContext directly in
  the VSA services; CatalogService keeps repositories as the Clean
  Architecture carve-out)
- v1-repository-pattern tag: pre-refactor state (textbook EF Repository
  pattern across all 5 services) preserved for teaching/comparison

Includes the canonical commands to browse and diff the two states.

The tag itself was created at commit 8c75b70 (the last commit on main
before this refactor branch) and pushed to origin. Anyone with the
repo cloned can `git fetch --tags && git checkout v1-repository-pattern`
to browse the older shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@emeraldleaf emeraldleaf force-pushed the refactor/simplicity-drop-repos-integration-tests branch from fc17711 to 4f3cc47 Compare May 25, 2026 23:40

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
CLAUDE.md (1)

97-99: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update stale VSA example that still names IOrderRepository.

This example now contradicts the direct-DbContext rule and can mislead future edits. Replace it with a non-repository shared-domain example.

As per coding guidelines **/*.cs / CLAUDE.md: VSA services drop EF repository wrappers and CLAUDE.md is the canonical source that paraphrases must stay aligned with.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLAUDE.md` around lines 97 - 99, Replace the stale repository example: remove
the reference to IOrderRepository and any wording suggesting EF/repository
wrappers and instead show a non-repository shared-domain example (e.g., a shared
value object or service like OrderId, PricingRules, or CurrencyConverter) while
keeping the existing mention of the Order aggregate and NotificationService as
context; update the sentence that currently reads "shared* across features (the
`Order` aggregate, `IOrderRepository`)" to something like "shared* across
features (the `Order` aggregate, shared value objects like `OrderId` or a
cross-cutting service such as `CurrencyConverter`)" so the doc no longer
endorses repository wrappers.
PaymentService/Features/ProcessPayment.cs (1)

91-102: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Outbox atomicity broken: SaveChangesAsync called before PublishAsync.

The current ordering persists the entity mutation before staging the outbox envelope. If the process crashes between SaveChangesAsync (line 92/109) and PublishAsync (line 94/114), the payment state is committed but the event is never enqueued — downstream sagas stall silently.

Under Wolverine's AutoApplyTransactions, PublishAsync stages the envelope in-memory; the subsequent SaveChangesAsync flushes both the entity change and the envelope into the same DB transaction. Calling SaveChangesAsync first defeats that guarantee.

🔧 Proposed fix — publish before save
         if (result.Success)
         {
             payment.MarkAsCompleted(result.TransactionId);
-            await context.SaveChangesAsync(cancellationToken);

             await eventPublisher.PublishAsync(new PaymentCompletedEvent
             {
                 PaymentId = payment.Id,
                 OrderId = payment.OrderId,
                 BuyerId = request.BuyerId,
                 Amount = payment.Amount,
                 Provider = payment.Provider,
                 CompletedAt = payment.CompletedAt!.Value
             }, cancellationToken);

+            await context.SaveChangesAsync(cancellationToken);
+
             PaymentsProcessed.Add(1, new KeyValuePair<string, object?>("outcome", "success"));
         }
         else
         {
             payment.MarkAsFailed(result.ErrorMessage ?? "Unknown error");
-            await context.SaveChangesAsync(cancellationToken);

             await eventPublisher.PublishAsync(new PaymentFailedEvent
             {
                 PaymentId = payment.Id,
                 OrderId = payment.OrderId,
                 BuyerId = request.BuyerId,
                 Reason = result.ErrorMessage ?? "Unknown error",
                 FailedAt = DateTime.UtcNow
             }, cancellationToken);

+            await context.SaveChangesAsync(cancellationToken);
+
             PaymentsProcessed.Add(1, new KeyValuePair<string, object?>("outcome", "failed"));
         }

As per coding guidelines: "Use the Outbox pattern for guaranteed event publishing: save entity + event in same transaction."

Also applies to: 108-121

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@PaymentService/Features/ProcessPayment.cs` around lines 91 - 102, The outbox
atomicity is broken because context.SaveChangesAsync is called before
eventPublisher.PublishAsync; change the order so you call
eventPublisher.PublishAsync(new PaymentCompletedEvent { ... }) before invoking
payment.MarkAsCompleted(...) persistence flush, ensuring the PublishAsync is
executed while AutoApplyTransactions/Outbox can stage the envelope in-memory and
then call context.SaveChangesAsync to commit both the entity change and the
envelope in one DB transaction; update the sequence around
payment.MarkAsCompleted, eventPublisher.PublishAsync, and
context.SaveChangesAsync (use the existing PaymentCompletedEvent,
payment.MarkAsCompleted, eventPublisher.PublishAsync, and
context.SaveChangesAsync symbols) so the event is published (staged) first and
then SaveChangesAsync commits both atomically.
ShippingService/Features/CreateShipment.cs (1)

38-67: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle concurrent unique-index races as an idempotent no-op in CreateShipmentHandler

  • ShippingDbContext enforces Shipment.OrderId as unique, but CreateShipmentHandler only does a pre-check + SaveChangesAsync without catching DbUpdateException; two concurrent at-least-once deliveries can race past the check and the loser will fail instead of returning the existing shipment id. Align with PaymentService/Features/ProcessPayment.cs, which catches DbUpdateException, re-fetches the winner, and returns its Id.
💡 Suggested fix
         await eventPublisher.PublishAsync(new ShipmentDispatchedEvent
         {
             ShipmentId = shipment.Id,
             OrderId = shipment.OrderId,
             Carrier = shipment.Carrier,
             TrackingNumber = shipment.TrackingNumber,
             DispatchedAt = shipment.DispatchedAt!.Value
         }, cancellationToken);

-        await context.SaveChangesAsync(cancellationToken);
+        try
+        {
+            await context.SaveChangesAsync(cancellationToken);
+        }
+        catch (DbUpdateException)
+        {
+            // Idempotency race: another delivery inserted the same OrderId first.
+            var existingId = await context.Shipments.AsNoTracking()
+                .Where(s => s.OrderId == request.OrderId)
+                .Select(s => (Guid?)s.Id)
+                .FirstOrDefaultAsync(cancellationToken);
+
+            if (existingId.HasValue)
+                return existingId.Value;
+
+            throw;
+        }

         ShipmentsDispatched.Add(1);
         return shipment.Id;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ShippingService/Features/CreateShipment.cs` around lines 38 - 67,
CreateShipmentHandler currently does a pre-check then SaveChangesAsync but
doesn't handle unique-index races; wrap the await
context.SaveChangesAsync(cancellationToken) (and the preceding
AddAsync/PublishAsync sequence) in a try/catch that catches DbUpdateException,
then in the catch re-query context.Shipments.FirstOrDefaultAsync(s => s.OrderId
== request.OrderId, cancellationToken) and if found return its Id (treating the
conflict as an idempotent no-op); if no existing shipment is found or the
exception is not a unique-index conflict, rethrow the exception. Ensure you
reference the existing symbols: CreateShipmentHandler, context.SaveChangesAsync,
context.Shipments, Shipment.Create/Dispatch, and the request.OrderId when
re-querying.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ShippingService/Features/GetShipmentByOrder.cs`:
- Around line 27-46: Include the ownership check in the EF query to avoid
materializing non-owner rows: in the query that builds ShipmentDto (the variable
shipment), add s.BuyerId == request.RequestingBuyerId to the Where predicate (so
it becomes .Where(s => s.OrderId == request.OrderId && s.BuyerId ==
request.RequestingBuyerId)) and then remove the later in-memory ownership guard
that returns shipment only when shipment.BuyerId == request.RequestingBuyerId;
keep the same null→404 behavior and continue projecting into
ShipmentDto/TrackingEventDto as before.

In `@tests/OrderService.Tests.Integration/OrderReadProjectionTests.cs`:
- Line 55: The tests fail because GetRequiredService<GetOrderByIdHandler>() and
GetRequiredService<GetOrdersByBuyerHandler>() are not registered in DI; fix by
either resolving OrderDbContext from the scope and new-ing the handlers in the
test (e.g., var db = scope.ServiceProvider.GetRequiredService<OrderDbContext>();
var handler = new GetOrderByIdHandler(db, ...)) or register the concrete
handlers on the test's IServiceCollection before building the scope so that
GetRequiredService<GetOrderByIdHandler>() and
GetRequiredService<GetOrdersByBuyerHandler>() succeed; update the test locations
referencing those handlers (lines referencing
GetOrderByIdHandler/GetOrdersByBuyerHandler) accordingly.

---

Outside diff comments:
In `@CLAUDE.md`:
- Around line 97-99: Replace the stale repository example: remove the reference
to IOrderRepository and any wording suggesting EF/repository wrappers and
instead show a non-repository shared-domain example (e.g., a shared value object
or service like OrderId, PricingRules, or CurrencyConverter) while keeping the
existing mention of the Order aggregate and NotificationService as context;
update the sentence that currently reads "shared* across features (the `Order`
aggregate, `IOrderRepository`)" to something like "shared* across features (the
`Order` aggregate, shared value objects like `OrderId` or a cross-cutting
service such as `CurrencyConverter`)" so the doc no longer endorses repository
wrappers.

In `@PaymentService/Features/ProcessPayment.cs`:
- Around line 91-102: The outbox atomicity is broken because
context.SaveChangesAsync is called before eventPublisher.PublishAsync; change
the order so you call eventPublisher.PublishAsync(new PaymentCompletedEvent {
... }) before invoking payment.MarkAsCompleted(...) persistence flush, ensuring
the PublishAsync is executed while AutoApplyTransactions/Outbox can stage the
envelope in-memory and then call context.SaveChangesAsync to commit both the
entity change and the envelope in one DB transaction; update the sequence around
payment.MarkAsCompleted, eventPublisher.PublishAsync, and
context.SaveChangesAsync (use the existing PaymentCompletedEvent,
payment.MarkAsCompleted, eventPublisher.PublishAsync, and
context.SaveChangesAsync symbols) so the event is published (staged) first and
then SaveChangesAsync commits both atomically.

In `@ShippingService/Features/CreateShipment.cs`:
- Around line 38-67: CreateShipmentHandler currently does a pre-check then
SaveChangesAsync but doesn't handle unique-index races; wrap the await
context.SaveChangesAsync(cancellationToken) (and the preceding
AddAsync/PublishAsync sequence) in a try/catch that catches DbUpdateException,
then in the catch re-query context.Shipments.FirstOrDefaultAsync(s => s.OrderId
== request.OrderId, cancellationToken) and if found return its Id (treating the
conflict as an idempotent no-op); if no existing shipment is found or the
exception is not a unique-index conflict, rethrow the exception. Ensure you
reference the existing symbols: CreateShipmentHandler, context.SaveChangesAsync,
context.Shipments, Shipment.Create/Dispatch, and the request.OrderId when
re-querying.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6ded0177-ae78-4141-9f2a-59c7144131a7

📥 Commits

Reviewing files that changed from the base of the PR and between 4d67057 and fc17711.

📒 Files selected for processing (38)
  • .claude/agents/architecture-reviewer.md
  • .coderabbit.yaml
  • CLAUDE.md
  • OrderService/Domain/IOrderRepository.cs
  • OrderService/Features/GetOrderById.cs
  • OrderService/Features/GetOrdersByBuyer.cs
  • OrderService/Features/PaymentCompletedHandler.cs
  • OrderService/Features/PaymentFailedHandler.cs
  • OrderService/Features/PlaceOrder.cs
  • OrderService/Features/ShipmentDispatchedHandler.cs
  • OrderService/Infrastructure/DependencyInjection.cs
  • OrderService/Infrastructure/OrderRepository.cs
  • PaymentService/Domain/IPaymentRepository.cs
  • PaymentService/Features/ProcessPayment.cs
  • PaymentService/Infrastructure/DependencyInjection.cs
  • PaymentService/Infrastructure/PaymentRecoveryJob.cs
  • PaymentService/Infrastructure/PaymentRepository.cs
  • README.md
  • ShippingService/Domain/IShipmentRepository.cs
  • ShippingService/Features/CreateShipment.cs
  • ShippingService/Features/GetShipmentByOrder.cs
  • ShippingService/Infrastructure/DependencyInjection.cs
  • ShippingService/Infrastructure/ShipmentRepository.cs
  • docs/STATUS.md
  • docs/code-flows/orderservice.md
  • docs/code-flows/paymentservice.md
  • docs/code-flows/shippingservice.md
  • tests/OrderService.Tests.Integration/OrderReadProjectionTests.cs
  • tests/OrderService.Tests.Unit/Application/GetOrderByIdHandlerTests.cs
  • tests/OrderService.Tests.Unit/Application/GetOrdersByBuyerHandlerTests.cs
  • tests/OrderService.Tests.Unit/Application/PaymentCompletedHandlerTests.cs
  • tests/OrderService.Tests.Unit/Application/PaymentFailedHandlerTests.cs
  • tests/OrderService.Tests.Unit/Application/PlaceOrderHandlerTests.cs
  • tests/OrderService.Tests.Unit/Application/ShipmentDispatchedHandlerTests.cs
  • tests/PaymentService.Tests.Unit/Application/PaymentRecoveryJobTests.cs
  • tests/PaymentService.Tests.Unit/Application/ProcessPaymentHandlerTests.cs
  • tests/ShippingService.Tests.Unit/Application/CreateShipmentHandlerTests.cs
  • tests/ShippingService.Tests.Unit/Application/GetShipmentByOrderHandlerTests.cs
💤 Files with no reviewable changes (16)
  • tests/OrderService.Tests.Unit/Application/PaymentFailedHandlerTests.cs
  • tests/OrderService.Tests.Unit/Application/ShipmentDispatchedHandlerTests.cs
  • tests/OrderService.Tests.Unit/Application/PlaceOrderHandlerTests.cs
  • ShippingService/Infrastructure/ShipmentRepository.cs
  • OrderService/Domain/IOrderRepository.cs
  • tests/OrderService.Tests.Unit/Application/GetOrderByIdHandlerTests.cs
  • ShippingService/Domain/IShipmentRepository.cs
  • tests/PaymentService.Tests.Unit/Application/PaymentRecoveryJobTests.cs
  • tests/ShippingService.Tests.Unit/Application/CreateShipmentHandlerTests.cs
  • tests/PaymentService.Tests.Unit/Application/ProcessPaymentHandlerTests.cs
  • tests/ShippingService.Tests.Unit/Application/GetShipmentByOrderHandlerTests.cs
  • PaymentService/Infrastructure/PaymentRepository.cs
  • tests/OrderService.Tests.Unit/Application/GetOrdersByBuyerHandlerTests.cs
  • tests/OrderService.Tests.Unit/Application/PaymentCompletedHandlerTests.cs
  • OrderService/Infrastructure/OrderRepository.cs
  • PaymentService/Domain/IPaymentRepository.cs

Comment thread ShippingService/Features/GetShipmentByOrder.cs Outdated
Comment thread tests/OrderService.Tests.Integration/OrderReadProjectionTests.cs
Wolverine's opts.Discovery builds its OWN internal message-type → handler-type
map for IMessageBus dispatch. It constructs handlers via IServiceScopeFactory
and never registers handler types in IServiceCollection. Production code paths
go through IMessageBus so this is invisible — but integration tests that
resolve handlers directly (GetRequiredService<MyHandler>()) throw
InvalidOperationException unless the handler is explicitly AddScoped<T>()'d
in AddXInfrastructure.

Failure mode that surfaced this gap: OrderReadProjectionTests broke in CI
after the repository-wrapper refactor — pre-refactor the tests resolved
IOrderRepository (which WAS registered), and the conversion to handler-
resolved tests missed the equivalent registration. Fix landed in the
preceding refactor commit; this commit encodes the lesson so the next person
standing up ShippingService.Tests.Integration or PaymentService.Tests.
Integration (both gaps tracked in STATUS.md) doesn't repeat it.

Encoded in 6 places per the Continuous Rule Encoding loop:

- CLAUDE.md "Communication Patterns" — new bullet with the full mechanism
  (Wolverine's container vs IServiceCollection, when each is used, the exact
  exception text, and the rule + reference template).
- CLAUDE.md "Testing" — sibling bullet to "Outbox-in-non-handler test" that
  references the Communication Patterns rule, naming the failure mode + the
  /check-rules audit pattern (GetRequiredService<*Handler>() ↔ AddScoped pair).
- .coderabbit.yaml **/*Test*.cs path_instruction — new HANDLER-DI REGISTRATION
  RULE section flagging two regression directions: tests adding a
  GetRequiredService<*Handler>() without the AddScoped pair, AND DI changes
  removing an AddScoped without removing the test resolution.
- .claude/agents/architecture-reviewer.md "When reviewing tests" — new Must-fix
  bullet covering both regression directions.
- docs/how-it-works.md §4 "The Wolverine pipeline" — new subsection "Two
  containers, not one" with the production vs test paths shown side-by-side
  and the one-line fix.
- docs/architecture.md "CQRS via Wolverine" — short callout pointing at the
  full mechanism in how-it-works.md + CLAUDE.md.
- README.md "Communication Patterns" — new subsection surfacing the gotcha to
  first-time contributors with the canonical fix snippet.

The rule is now (a) in the canonical hard-rules file, (b) reviewed on every
test PR by CodeRabbit AND architecture-reviewer, (c) explained with full
mechanism in the architecture walkthrough, and (d) visible from the README so
no one ever has to debug the InvalidOperationException from scratch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
coderabbitai[bot]
coderabbitai Bot previously requested changes May 26, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/STATUS.md`:
- Line 41: Update the conflicting unit-test counts in the STATUS document so
there's a single canonical number: find the phrase "**Build clean; 142 unit
tests pass**" and the other occurrence "**134/134**" and reconcile them to the
correct current total (pick one canonical value), then replace the other
occurrence so both statements show the same unit-test count.

In `@PaymentService/Features/ProcessPayment.cs`:
- Around line 55-58: Current early-return (in ProcessPayment handler) on finding
an existing payment (var existing = await
context.Payments.FirstOrDefaultAsync(...)) can hide missing event publishes;
change the logic so that instead of simply returning existing.Id you inspect
existing.Status (e.g., PaymentStatus.Completed or PaymentStatus.Failed) and, if
terminal, attempt to re-publish the corresponding event (PaymentCompletedEvent
or PaymentFailedEvent) via the same PublishAsync path used for new payments
(ensuring PublishAsync is idempotent or guarded by an “event published” marker)
before returning; apply the same reconciliation approach to the other
terminal-state branch around the 92-122 section so retries will re-emit events
rather than silently short-circuiting.

In `@ShippingService/Features/CreateShipment.cs`:
- Around line 38-41: The current pre-check using
context.Shipments.FirstOrDefaultAsync(s => s.OrderId == request.OrderId) races
under concurrent inserts and can cause a unique-index failure on
SaveChangesAsync; make the handler idempotent by keeping the existing pre-check
but wrapping the SaveChangesAsync that creates the new Shipment in a try/catch
that catches the database update/unique-constraint exception (e.g.,
DbUpdateException), and in the catch re-query
context.Shipments.FirstOrDefaultAsync(s => s.OrderId == request.OrderId,
cancellationToken) and return the found shipment.Id if present; only rethrow the
exception if the re-query does not find an existing shipment. Ensure you
reference the same request.OrderId, context.Shipments, the existing variable and
SaveChangesAsync call, and use the cancellationToken for the re-query.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d5a995f0-f079-405e-bd29-9fc7622e8f77

📥 Commits

Reviewing files that changed from the base of the PR and between fc17711 and 172d52c.

📒 Files selected for processing (40)
  • .claude/agents/architecture-reviewer.md
  • .coderabbit.yaml
  • CLAUDE.md
  • OrderService/Domain/IOrderRepository.cs
  • OrderService/Features/GetOrderById.cs
  • OrderService/Features/GetOrdersByBuyer.cs
  • OrderService/Features/PaymentCompletedHandler.cs
  • OrderService/Features/PaymentFailedHandler.cs
  • OrderService/Features/PlaceOrder.cs
  • OrderService/Features/ShipmentDispatchedHandler.cs
  • OrderService/Infrastructure/DependencyInjection.cs
  • OrderService/Infrastructure/OrderRepository.cs
  • PaymentService/Domain/IPaymentRepository.cs
  • PaymentService/Features/ProcessPayment.cs
  • PaymentService/Infrastructure/DependencyInjection.cs
  • PaymentService/Infrastructure/PaymentRecoveryJob.cs
  • PaymentService/Infrastructure/PaymentRepository.cs
  • README.md
  • ShippingService/Domain/IShipmentRepository.cs
  • ShippingService/Features/CreateShipment.cs
  • ShippingService/Features/GetShipmentByOrder.cs
  • ShippingService/Infrastructure/DependencyInjection.cs
  • ShippingService/Infrastructure/ShipmentRepository.cs
  • docs/STATUS.md
  • docs/architecture.md
  • docs/code-flows/orderservice.md
  • docs/code-flows/paymentservice.md
  • docs/code-flows/shippingservice.md
  • docs/how-it-works.md
  • tests/OrderService.Tests.Integration/OrderReadProjectionTests.cs
  • tests/OrderService.Tests.Unit/Application/GetOrderByIdHandlerTests.cs
  • tests/OrderService.Tests.Unit/Application/GetOrdersByBuyerHandlerTests.cs
  • tests/OrderService.Tests.Unit/Application/PaymentCompletedHandlerTests.cs
  • tests/OrderService.Tests.Unit/Application/PaymentFailedHandlerTests.cs
  • tests/OrderService.Tests.Unit/Application/PlaceOrderHandlerTests.cs
  • tests/OrderService.Tests.Unit/Application/ShipmentDispatchedHandlerTests.cs
  • tests/PaymentService.Tests.Unit/Application/PaymentRecoveryJobTests.cs
  • tests/PaymentService.Tests.Unit/Application/ProcessPaymentHandlerTests.cs
  • tests/ShippingService.Tests.Unit/Application/CreateShipmentHandlerTests.cs
  • tests/ShippingService.Tests.Unit/Application/GetShipmentByOrderHandlerTests.cs
💤 Files with no reviewable changes (16)
  • PaymentService/Domain/IPaymentRepository.cs
  • tests/PaymentService.Tests.Unit/Application/PaymentRecoveryJobTests.cs
  • OrderService/Infrastructure/OrderRepository.cs
  • tests/ShippingService.Tests.Unit/Application/CreateShipmentHandlerTests.cs
  • tests/OrderService.Tests.Unit/Application/GetOrderByIdHandlerTests.cs
  • OrderService/Domain/IOrderRepository.cs
  • ShippingService/Domain/IShipmentRepository.cs
  • tests/OrderService.Tests.Unit/Application/GetOrdersByBuyerHandlerTests.cs
  • tests/OrderService.Tests.Unit/Application/PaymentFailedHandlerTests.cs
  • PaymentService/Infrastructure/PaymentRepository.cs
  • tests/ShippingService.Tests.Unit/Application/GetShipmentByOrderHandlerTests.cs
  • tests/OrderService.Tests.Unit/Application/PlaceOrderHandlerTests.cs
  • ShippingService/Infrastructure/ShipmentRepository.cs
  • tests/OrderService.Tests.Unit/Application/PaymentCompletedHandlerTests.cs
  • tests/PaymentService.Tests.Unit/Application/ProcessPaymentHandlerTests.cs
  • tests/OrderService.Tests.Unit/Application/ShipmentDispatchedHandlerTests.cs

Comment thread docs/STATUS.md Outdated
Comment thread PaymentService/Features/ProcessPayment.cs
Comment thread ShippingService/Features/CreateShipment.cs
emeraldleaf and others added 4 commits May 25, 2026 19:16
…ApiFactory

CI symptom: every test in OrderService.Tests.Integration PASSED (4 OrderSagaTests
+ 4 OrderReadProjectionTests = 8/8), but xUnit reported a "Test Class Cleanup
Failure (OrderSagaTests) System.Threading.Tasks.TaskCanceledException" and
dotnet test exited non-zero.

Root cause: Wolverine's durable agents (outbox dispatcher, scheduled-message
agent, listener heartbeats) can outlive the host's default shutdown grace
period under CI's slower scheduling. When they do, the cancellation propagates
out through base.DisposeAsync(), xUnit catches it as a class-cleanup failure,
and the runner exits 1 even though every test passed.

Fix: wrap base.DisposeAsync() in a try/catch (OperationCanceledException) —
TaskCanceledException inherits from it, so the single catch covers both. The
catch body has a comment to satisfy S108 (empty-block). Tests have already
completed by this point; a delayed background-service shutdown isn't a
correctness signal worth failing the build on.

The dispose order is unchanged — host first, then SQL container — because
that's the fix for the OTHER teardown failure mode where Wolverine's
DurableReceiver kept polling the outbox tables while the SQL container was
torn down. Both fixes coexist here:
  1. Order: host → SQL container (so the receiver gets clean shutdown signal)
  2. Catch: OperationCanceledException (so a late-draining agent doesn't fail CI)

If teardown ever needs to be debugged, swap the empty catch for a log statement
to surface what's not shutting down in time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… hard gate

PR #30 (simplicity refactor: drop EF repository wrappers) fails Codecov's
patch check at 39.55% vs the 59.91% auto-target. The dip is structural,
not a regression: the refactor deletes mocked-handler unit tests in favor
of integration tests against Testcontainers. Codecov can't tell that the
deleted Substitute.For<IFooRepository> tests had effectively zero load-
bearing assertions, while the existing integration suite catches the real
failure modes (cartesian rows, concurrency tokens, cache invalidation,
IDOR contracts).

Two services are real open gaps tracked in docs/STATUS.md "Open issues":
ShippingService and PaymentService both need .Tests.Integration projects
stood up before their handler write paths show as covered. Until then, a
patch-coverage requirement blocks refactor PRs on a metric misaligned with
the testing strategy.

This config:
  - patch: informational (still reports the percentage as a PR comment,
    doesn't block CI)
  - project: stays as the hard gate with target=auto and a 1% threshold
    for tolerance against small fluctuations
  - comment: keeps the per-line uncovered-files breakdown visible during
    review so the missing coverage is still surfaced

The YAML carries a long header comment explaining the rationale in case
the next maintainer wonders why patch is "soft" — short answer is "the
testing philosophy explicitly rejects mocked-handler unit tests, but
Codecov's patch metric can't see that distinction."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Four findings from CodeRabbit's review of PR #30 (refactor/simplicity-drop-
repos-integration-tests), each fixed surgically:

1. ShippingService/Features/GetShipmentByOrder.cs — IDOR predicate now lives
   in the EF Where clause itself (s.OrderId == request.OrderId AND
   s.BuyerId == request.RequestingBuyerId) rather than filtering the
   materialized DTO in C#. Non-owner rows never cross the wire. External
   contract unchanged (null → 404 anti-enumeration); SQL is tighter.

2. ShippingService/Features/CreateShipment.cs — duplicate-insert race
   handler added (mirrors the existing pattern in ProcessPayment). When two
   at-least-once redeliveries race past the pre-check, the unique-OrderId
   index trips DbUpdateException on the loser's SaveChangesAsync; we detach
   the orphaned entity, re-fetch the winner, and return its ID. Wolverine's
   UseEntityFrameworkCoreTransactions bridge rolls back the staged
   ShipmentDispatchedEvent envelope with the failed save, so the loser
   doesn't double-publish.

3. PaymentService/Features/ProcessPayment.cs — defensive event re-publish
   on terminal-state re-entry. Adds a private RepublishTerminalEventAsync
   helper invoked when the existence check finds a Payment already
   Completed or Failed. Pending stays a no-op (original processing may be
   in-flight; PaymentRecoveryJob will eventually handle stuck Pendings).
   Wolverine's AutoApplyTransactions usually prevents the "saved Payment,
   missing event" gap CodeRabbit flagged — the entity write and the
   outbox-envelope write commit together. But manual DB intervention,
   outbox-table cleanup, or future code changes that break the outbox
   guarantees could leave a terminal Payment with no enqueued event and
   stall the saga (OrderService waits forever for PaymentCompletedEvent).
   The re-publish is defense in depth; downstream handlers' status guards
   make the eventual double-publish a no-op.

4. docs/STATUS.md — harmonized two conflicting unit-test totals (line 41
   said 142 / down from 165, line 86 said 134/134). Reran tests on this
   branch: actual count is 121 (45 OrderService + 38 CatalogService +
   19 PaymentService + 11 ShippingService + 8 NotificationService).

Build clean (0 warnings, 0 errors); 121/121 unit tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@emeraldleaf

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/STATUS.md (1)

5-5: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update Last updated to today’s date.

Line 5 is stale for this diff. Since this file changed in this PR, set it to 2026-05-26.

As per coding guidelines, "docs/STATUS.md: STATUS.md is the cross-session entry point. When this changes, the "Last updated:" date should be today."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/STATUS.md` at line 5, Update the "**Last updated:**" entry in
docs/STATUS.md from "2026-05-25" to "2026-05-26" by editing the line that begins
with "**Last updated:**" (the line containing the parenthetical note about the
simplicity refactor and CatalogService) so the date reflects today.
♻️ Duplicate comments (2)
PaymentService/Features/ProcessPayment.cs (1)

70-72: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Apply the terminal-state re-publish repair in the insert-race fallback too.

The new helper only covers the first existing fast-path. Line 93 can still return a terminal winner from the DbUpdateException refetch without calling RepublishTerminalEventAsync, so a concurrent redelivery can recreate the same “saved row, missing event” gap this change is trying to heal.

💡 Minimal fix
         catch (DbUpdateException)
         {
             // The pre-check above races with concurrent deliveries: two messages can both see
             // "no existing payment" and both try to insert. The unique index on OrderId catches
             // the loser. Detach our about-to-be-orphaned entity, re-fetch the winner, and
             // return its ID. Without this catch, the redelivery model leaks DbUpdateException
             // to Wolverine's retry loop on every concurrent insert.
             context.Entry(payment).State = EntityState.Detached;
             var racedExisting = await context.Payments
                 .FirstOrDefaultAsync(p => p.OrderId == request.OrderId, cancellationToken);
             if (racedExisting is not null)
+            {
+                await RepublishTerminalEventAsync(racedExisting, cancellationToken);
                 return racedExisting.Id;
+            }
             throw;
         }

Also applies to: 145-176

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@PaymentService/Features/ProcessPayment.cs` around lines 70 - 72, The
insert-race fallback path inside the DbUpdateException catch (the refetch that
may set the local variable existing to a terminal winner) must also invoke
RepublishTerminalEventAsync to heal the “saved row, missing event” gap; update
the DbUpdateException refetch branch (the insert-race fallback logic) to detect
when existing is in a terminal state and call await
RepublishTerminalEventAsync(existing, cancellationToken) before returning, and
apply the same change to the other similar fallback area around the 145-176
region so both fast-path and insert-race recovery paths call
RepublishTerminalEventAsync when existing is terminal.
PaymentService/Infrastructure/PaymentRecoveryJob.cs (1)

164-172: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep the per-row fault boundary exhaustive.

This now isolates only three exception types. Any other failure from RecoverOneAsync—for example around PublishAsync, transaction open/commit, or provider-specific database errors—will still break out of the loop and abandon the rest of staleIds for this sweep.

💡 Minimal fix
             catch (TimeoutException ex)
             {
                 LogRowFailed(logger, ex, id);
             }
+            catch (Exception ex)
+            {
+                LogRowFailed(logger, ex, id);
+            }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@PaymentService/Infrastructure/PaymentRecoveryJob.cs` around lines 164 - 172,
The per-row error handling in PaymentRecoveryJob currently only catches
DbUpdateException, InvalidOperationException and TimeoutException, so other
exceptions inside RecoverOneAsync (including PublishAsync, transaction
open/commit, or provider-specific errors) will break the loop and stop
processing remaining staleIds; add a final catch (Exception ex) in the same
try/catch block that calls LogRowFailed(logger, ex, id) (same behavior as the
other catches) so any unexpected exception for a given id is logged and the loop
continues processing the rest of staleIds in RecoverOneAsync/where the loop over
staleIds is implemented.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@docs/STATUS.md`:
- Line 5: Update the "**Last updated:**" entry in docs/STATUS.md from
"2026-05-25" to "2026-05-26" by editing the line that begins with "**Last
updated:**" (the line containing the parenthetical note about the simplicity
refactor and CatalogService) so the date reflects today.

---

Duplicate comments:
In `@PaymentService/Features/ProcessPayment.cs`:
- Around line 70-72: The insert-race fallback path inside the DbUpdateException
catch (the refetch that may set the local variable existing to a terminal
winner) must also invoke RepublishTerminalEventAsync to heal the “saved row,
missing event” gap; update the DbUpdateException refetch branch (the insert-race
fallback logic) to detect when existing is in a terminal state and call await
RepublishTerminalEventAsync(existing, cancellationToken) before returning, and
apply the same change to the other similar fallback area around the 145-176
region so both fast-path and insert-race recovery paths call
RepublishTerminalEventAsync when existing is terminal.

In `@PaymentService/Infrastructure/PaymentRecoveryJob.cs`:
- Around line 164-172: The per-row error handling in PaymentRecoveryJob
currently only catches DbUpdateException, InvalidOperationException and
TimeoutException, so other exceptions inside RecoverOneAsync (including
PublishAsync, transaction open/commit, or provider-specific errors) will break
the loop and stop processing remaining staleIds; add a final catch (Exception
ex) in the same try/catch block that calls LogRowFailed(logger, ex, id) (same
behavior as the other catches) so any unexpected exception for a given id is
logged and the loop continues processing the rest of staleIds in
RecoverOneAsync/where the loop over staleIds is implemented.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d60f1f11-24cc-4ebc-912b-6fdce120a676

📥 Commits

Reviewing files that changed from the base of the PR and between 34e0e47 and 065b4e3.

📒 Files selected for processing (6)
  • PaymentService/Features/ProcessPayment.cs
  • PaymentService/Infrastructure/PaymentRecoveryJob.cs
  • ShippingService/Features/CreateShipment.cs
  • ShippingService/Features/GetShipmentByOrder.cs
  • codecov.yml
  • docs/STATUS.md

Two findings from CodeRabbit's review that the previous fix commit missed:

1. CLAUDE.md "Why feature folders work here" example — the phrase
   "shared across features (the Order aggregate, IOrderRepository)" still
   referenced IOrderRepository as if it were a current artifact. This very
   PR deletes IOrderRepository (and IPaymentRepository / IShipmentRepository /
   IProductRepository). Updated the example to enumerate what actually
   survives in Domain folders post-refactor: aggregates, value objects, and
   consumer-substitution ports (IEventPublisher, ICatalogClient). The old
   wording was self-contradicting against the rest of the rule.

2. PaymentService/Features/ProcessPayment.cs — outbox atomicity ordering
   bug in both the Success and Failed branches. CodeRabbit's analysis
   matches the Wolverine documentation: UseEntityFrameworkCoreTransactions
   stages outbox envelopes when PublishAsync runs, then SaveChangesAsync
   flushes BOTH the entity change AND the staged envelope to
   wolverine.outgoing_envelopes IN THE SAME DB TRANSACTION. If we save
   first and publish after, the entity commits alone — leaving a brief
   window where the Payment row exists but no event has been enqueued
   (the envelope only persists on the NEXT SaveChanges, which is
   Wolverine's automatic post-handler call). A process death between
   those two calls leaves a saved Completed payment with no event;
   the saga stalls because OrderService waits forever for
   PaymentCompletedEvent. The retry's existence-check would short-circuit
   past the new-payment path without ever firing the event.

   The previous commit (065b4e3) added RepublishTerminalEventAsync as
   a defense-in-depth backstop — it re-publishes the event when an
   existing terminal-state payment is found on retry. That stays in
   place because it also covers edge cases like manual DB intervention.
   But the right structural fix is the order swap: publish BEFORE save
   so the atomicity guarantee actually works. CreateShipment already had
   the correct order; ProcessPayment didn't.

Build clean (0 warnings, 0 errors); 121/121 unit tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@emeraldleaf

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
CLAUDE.md (1)

97-101: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add the required rule-paraphrase audit note for this CLAUDE.md edit.

Please include this exact sentence in the PR context: “Run /check-rules locally to audit paraphrases against this diff.”

As per coding guidelines CLAUDE.md: “Flag the CLAUDE.md change with a note: "Run /check-rules locally to audit paraphrases against this diff."”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLAUDE.md` around lines 97 - 101, Add the required audit-note sentence to the
PR context for the CLAUDE.md change: insert the exact sentence “Run /check-rules
locally to audit paraphrases against this diff.” as a rule-paraphrase audit note
associated with the CLAUDE.md edit (the paragraph describing shared* across
features — aggregates, value objects, and consumer-substitution ports). Ensure
the sentence appears verbatim in the PR description or commit message so the
check is discoverable.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@CLAUDE.md`:
- Around line 97-101: Add the required audit-note sentence to the PR context for
the CLAUDE.md change: insert the exact sentence “Run /check-rules locally to
audit paraphrases against this diff.” as a rule-paraphrase audit note associated
with the CLAUDE.md edit (the paragraph describing shared* across features —
aggregates, value objects, and consumer-substitution ports). Ensure the sentence
appears verbatim in the PR description or commit message so the check is
discoverable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 5d54bbf1-9d30-4455-850f-bbdc34fc6b1c

📥 Commits

Reviewing files that changed from the base of the PR and between 065b4e3 and 568c3c3.

📒 Files selected for processing (2)
  • CLAUDE.md
  • PaymentService/Features/ProcessPayment.cs

@emeraldleaf

Copy link
Copy Markdown
Owner Author

Ran /check-rules locally to audit "See CLAUDE.md" paraphrases against this diff.
No drift detected. The only paraphrases of the changed CLAUDE.md sections are
in the per-service Infrastructure/DependencyInjection.cs files, all aligned
with the canonical "Data access: DbContext directly" rule title.

All code-level findings from prior CodeRabbit reviews
(4359116470, 4359298567, 4359355610, 4360164087) are addressed:

  • CLAUDE.md duplicate outbox-atomic bullet → removed (2d1a9d5)
  • CLAUDE.md stale IOrderRepository example → updated (568c3c3)
  • ProcessPayment race condition → DbUpdateException catch + re-fetch (065b4e3)
  • ProcessPayment save-before-publish order → reordered (568c3c3)
  • PaymentRecoveryJob single-scope reuse → per-row scope + try/catch (065b4e3)
  • CreateShipment race condition → DbUpdateException catch + re-fetch (065b4e3)
  • GetShipmentByOrder IDOR predicate → moved into EF Where clause (065b4e3)
  • OrderReadProjectionTests DI registration → handlers AddScoped'd (earlier in branch)

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown

@emeraldleaf Thanks for the thorough audit log — that's a clean checklist. Triggering the review now to verify everything across the full diff.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@emeraldleaf emeraldleaf dismissed coderabbitai[bot]’s stale review May 26, 2026 03:37

All findings from prior CodeRabbit reviews on earlier commits are addressed
in 568c3c3 (latest head). Most recent re-review completed with no actionable
items. Dismissing the stale "Request changes" status from review 4359116470
so the merge can proceed.

@emeraldleaf emeraldleaf requested a review from Copilot May 26, 2026 03:39

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR continues the “simplicity refactor” across the VSA services by removing EF repository wrappers and shifting handlers to use DbContext directly, while aligning documentation/testing guidance and adjusting integration test infrastructure accordingly.

Changes:

  • Removed I*Repository abstractions and EF repository implementations in Order/Payment/Shipping; handlers now query/project/mutate via DbContext.
  • Updated Payment/Shipping/Order handler logic for idempotency and race handling (unique-index conflicts) and inlined the outbox-atomic transaction wrapper in PaymentRecoveryJob.
  • Reworked the testing approach by deleting mocked-repository handler unit tests, updating Order integration tests, and adding a Codecov configuration + more resilient integration test teardown.

Reviewed changes

Copilot reviewed 42 out of 42 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
tests/ShippingService.Tests.Unit/Application/GetShipmentByOrderHandlerTests.cs Removed mocked-repository unit tests for shipment read handler.
tests/ShippingService.Tests.Unit/Application/CreateShipmentHandlerTests.cs Removed mocked-repository unit tests for shipment creation handler.
tests/PaymentService.Tests.Unit/Application/ProcessPaymentHandlerTests.cs Removed mocked-repository unit tests for payment processing handler.
tests/PaymentService.Tests.Unit/Application/PaymentRecoveryJobTests.cs Removed unit tests covering recovery sweeper behavior.
tests/OrderService.Tests.Unit/Application/ShipmentDispatchedHandlerTests.cs Removed mocked-repository unit tests for shipment dispatched event handler.
tests/OrderService.Tests.Unit/Application/PlaceOrderHandlerTests.cs Removed mocked-repository unit tests for order placement handler.
tests/OrderService.Tests.Unit/Application/PaymentFailedHandlerTests.cs Removed mocked-repository unit tests for payment failed event handler.
tests/OrderService.Tests.Unit/Application/PaymentCompletedHandlerTests.cs Removed mocked-repository unit tests for payment completed event handler.
tests/OrderService.Tests.Unit/Application/GetOrdersByBuyerHandlerTests.cs Removed mocked-repository unit tests for buyer orders query handler.
tests/OrderService.Tests.Unit/Application/GetOrderByIdHandlerTests.cs Removed mocked-repository unit tests for order-by-id query handler.
tests/OrderService.Tests.Integration/OrderReadProjectionTests.cs Updated read-projection tests to resolve/query handlers directly instead of repositories.
tests/OrderService.Tests.Integration/OrderApiFactory.cs Made integration test teardown more resilient by swallowing cancellation during host shutdown.
ShippingService/Infrastructure/ShipmentRepository.cs Deleted EF repository implementation (DbContext used directly by handlers).
ShippingService/Infrastructure/DependencyInjection.cs Removed repository registration; docs updated to “DbContext directly” guidance.
ShippingService/Features/GetShipmentByOrder.cs Switched to inline EF projection + buyer predicate in SQL for IDOR prevention.
ShippingService/Features/CreateShipment.cs Switched to DbContext writes and added unique-index race recovery around SaveChanges.
ShippingService/Domain/IShipmentRepository.cs Deleted repository interface.
README.md Documented the “two architectural eras” and the refactor/testing philosophy.
PaymentService/Infrastructure/PaymentRepository.cs Deleted EF repository implementation.
PaymentService/Infrastructure/PaymentRecoveryJob.cs Reworked sweeper to use DbContext directly, per-row scopes, and explicit transaction for outbox atomicity.
PaymentService/Infrastructure/DependencyInjection.cs Removed repository registration; clarified where the outbox-atomic wrapper now lives.
PaymentService/Features/ProcessPayment.cs Switched to DbContext directly; added unique-index race recovery + terminal-state republish helper.
PaymentService/Domain/IPaymentRepository.cs Deleted repository interface (including ExecuteInTransactionAsync).
OrderService/Infrastructure/OrderRepository.cs Deleted EF repository implementation (DbContext used directly by handlers).
OrderService/Infrastructure/DependencyInjection.cs Removed repository registration; added scoped registrations for read handlers to support integration tests.
OrderService/Features/ShipmentDispatchedHandler.cs Updated to load/mutate via OrderDbContext directly.
OrderService/Features/PlaceOrder.cs Updated to write via OrderDbContext directly (and explicit SaveChanges).
OrderService/Features/PaymentFailedHandler.cs Updated to load/mutate via OrderDbContext directly.
OrderService/Features/PaymentCompletedHandler.cs Updated to load/mutate via OrderDbContext directly.
OrderService/Features/GetOrdersByBuyer.cs Inlined EF projection + defense-in-depth pagination clamp in handler.
OrderService/Features/GetOrderById.cs Inlined EF projection in handler.
OrderService/Domain/IOrderRepository.cs Deleted repository interface.
docs/STATUS.md Added/expanded refactor entry; updated unit test counts and test philosophy notes.
docs/how-it-works.md Added explanation of Wolverine handler discovery vs DI registration (“two containers”).
docs/code-flows/shippingservice.md Updated diagrams and narrative for DbContext-direct approach (some IDOR-read flow text still needs alignment).
docs/code-flows/paymentservice.md Updated diagrams/narrative for DbContext-direct approach and inline recovery transaction pattern.
docs/code-flows/orderservice.md Updated diagrams/narrative for DbContext-direct approach and DI registration nuance.
docs/architecture.md Added note about Wolverine handler discovery vs DI registration.
codecov.yml Added Codecov configuration (project gate + informational patch coverage).
CLAUDE.md Codified “DbContext directly, no repository wrappers” and handler DI-registration rule for tests.
.coderabbit.yaml Updated review instructions to flag repository wrappers and enforce the new testing/DI rules.
.claude/agents/architecture-reviewer.md Updated agent heuristics for DbContext-direct handlers, test strategy, and DI registration caveat.

Comment thread OrderService/Features/PlaceOrder.cs Outdated
Comment thread docs/code-flows/shippingservice.md
Comment thread docs/code-flows/orderservice.md
Comment thread tests/OrderService.Tests.Integration/OrderReadProjectionTests.cs
Comment thread PaymentService/Features/ProcessPayment.cs
Comment thread ShippingService/Features/CreateShipment.cs
Comment thread PaymentService/Infrastructure/PaymentRecoveryJob.cs
Comment thread ShippingService/Features/GetShipmentByOrder.cs
emeraldleaf and others added 2 commits May 25, 2026 21:51
…redicate

Copilot's PR review flagged docs/code-flows/shippingservice.md as having
"IDOR-read flow text still needs alignment." The Flow 2 sequence diagram and
explanatory text described the pre-refactor pattern where the handler filtered
by OrderId only, then did a separate in-memory BuyerId check on the
materialized DTO. After 065b4e3 the predicate moved into the EF Where clause —
the handler now runs a single FirstOrDefaultAsync with BOTH OrderId AND
BuyerId in SQL, and non-owner rows never cross the wire.

Mermaid sequence updated to show the combined Where clause + a Note clarifying
that null covers both "no shipment" and "shipment exists but not yours". The
"Why null → 404" + "Why the ownership check lives in the SQL predicate" prose
rewritten to match the actual code. Same external null → 404 contract; just
the explanation now matches what the code does.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… outbox atomicity

Copilot flagged the same Save-before-Publish ordering bug here that
CodeRabbit caught in ProcessPayment (fixed in 568c3c3). The class-summary
comment claimed SaveChanges "binds the entity write + staged OrderPlacedEvent
envelope" but PublishAsync was called AFTER SaveChanges — meaning the
envelope was staged into Wolverine's tracker only after the Order row had
already committed alone. Wolverine's post-handler automatic SaveChanges
flushes the staged envelope on a separate transaction, leaving a brief
"Order in DB but no event enqueued" window where a process death stalls
PaymentService (it never sees OrderPlacedEvent → never invokes
ProcessPayment → saga dies).

Fix: reorder so PublishAsync runs FIRST (stages the envelope), then
SaveChangesAsync flushes BOTH the Order row AND the staged envelope into
the same DB transaction via UseEntityFrameworkCoreTransactions. Same shape
as the ProcessPayment, CreateShipment fixes earlier in this branch.

Also cleaned up two stale references in the class summary:
- The numbered list said "Persist the order via the repository" — there is
  no repository anymore (deleted in 5e9f7ec). Now says "Add the aggregate
  to the tracked DbContext."
- The "Transactional outbox" paragraph referenced `orderRepository.AddAsync`
  which doesn't exist. Rewritten to describe the actual publish-before-save
  ordering and why it matters for atomicity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@emeraldleaf emeraldleaf merged commit eda94b5 into main May 26, 2026
7 checks passed
emeraldleaf added a commit that referenced this pull request May 27, 2026
… follow-up

Three new rules + one rule tightening + one open-issue acknowledgement,
all sourced from two Milan Jovanović / Kerim Kara articles on scaling
patterns. PR scope continues to be "encode the rules before the next
instance hits review."

Rules added:

1. NON-SARGABLE PREDICATES DEFEAT INDEXES — fix at write time.
   A Where(...) that wraps a column in a function (u.Email.ToLower() == x,
   o.CreatedAt.Date == today, EF.Functions.ILike with leading wildcard)
   defeats any B-tree index on that column. Fix at write time: normalize
   on insert/update + Where against the normalized column, or use
   case-insensitive column collation. Leading-wildcard substring search
   isn't B-tree-indexable in any database — escalate to Postgres tsvector
   full-text or a dedicated search engine when load justifies it.
   CatalogService/Features/SearchProducts.cs already documents the
   leading-wildcard trade-off explicitly; updated its cross-reference to
   point at the new rule by name.

2. FAN-OUT BELONGS ON THE MESSAGE BUS, NOT IN A SYNCHRONOUS HANDLER LOOP.
   A handler that iterates a recipient list inline and awaits a sender
   per recipient holds the request open for N × per-recipient-latency
   and concentrates work on one process. Right shape: publish one
   Wolverine message per recipient (or per batch of K), return immediately,
   let per-recipient handlers run in parallel under
   MaxDegreeOfParallelism throttle. Preventative — not retroactively
   violated today (NotificationService receives one event = one outbound
   notification).

3. PARALLELIZE INDEPENDENT AWAITS WITH TASK.WHENALL.
   Sequential await of independent I/O calls serializes latency for free.
   When a handler makes N independent I/O calls (N gRPC to different
   services, N HTTP to different external APIs, N queries against
   DIFFERENT DbContexts), Task.WhenAll pays the max latency, not the sum.
   Reference: OrderService/Features/PlaceOrder.cs:93 (gRPC fan-out over
   per-line GetProductAsync with the DbContext-safety caveat documented
   inline). Explicit caveats encoded: don't WhenAll dependent operations,
   operations sharing the same DbContext scope (not thread-safe), or
   operations whose per-call failure observability matters (WhenAll
   surfaces only the first exception).

Rule tightening (from prior architecture-reviewer feedback):

4. 202 ACCEPTED RULE — two definitional clarifications.
   (a) "Tracking row" can be the aggregate itself when its ID is the
       polling key (POST /api/v1/orders pattern), not necessarily a
       separate jobs table.
   (b) "Commit atomically" branches by dispatch path: bus.InvokeAsync
       gets AutoApplyTransactions for free; inline persist+publish needs
       the explicit BeginTransactionAsync → SaveChangesAsync → CommitAsync
       wrap from the existing Outbox-outside-handler rule. Cross-reference
       added.

STATUS.md follow-up:

5. POST /api/v1/payments/process noted as a partial Should-consider
   under the new long-running-work rule. Endpoint returns 202 but the
   handler awaits Stripe synchronously; typical sub-second but tail
   latency on degraded states is seconds-to-30s. Deferred (current
   shape works for the demo; rule is encoded preventatively).

Note on the second Kerim Kara article ("Every Senior .NET Developer Has
Defended This Architecture"): most of its lessons are already encoded in
NextAurora — the I*Repository deletion (PR #30), the CatalogService
Clean→VSA collapse (PR #31), the "Interfaces earn their keep through
consumer substitution" rule, the integration-tests-over-mocked-repository
discipline, the DbContext-direct data-access shape. The article reads
as a victory lap for the simplicity refactor. The one net-new takeaway
was the sequential-awaits trap, encoded here as rule #3.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
emeraldleaf added a commit that referenced this pull request May 27, 2026
…#39)

* chore(claude): encode Factory Pattern / keyed-services rule across config

Adds the "factory pattern earns its keep at 2+ impls" rule to the three
canonical encoding sites + a worked sketch at the reference site:

- CLAUDE.md: new bullet right after the consumer-substitution rule.
  Codifies that .NET keyed services (AddKeyedScoped + [FromKeyedServices])
  is the canonical multi-impl factory; don't hand-roll IPortFactory; don't
  pre-build the factory while there's only one impl. NotificationService
  cited as the canonical "ready for the factory, not yet wearing it" case.
- .claude/agents/architecture-reviewer.md: new section keyed to
  Infrastructure/DependencyInjection.cs files. Two flagged patterns:
  premature factory (Must-fix) and missing factory at 2+ impls
  (Should-consider). Explicit guidance that IServiceProvider's keyed-
  services API IS the canonical factory.
- .coderabbit.yaml: matching path_instructions block under
  **/Infrastructure/DependencyInjection.cs, encoding the same two
  patterns so CodeRabbit catches future violations at PR-review time.
- NotificationService/Infrastructure/DependencyInjection.cs: composition-
  root XML doc now sketches the future keyed-services rewrite that the
  current single-AddScoped will become when SendGrid/Twilio actually
  ship — so the rule lands with a concrete reference shape in code, not
  just in config.

Why now: PR #30 + #31 collapsed the I*Repository wrappers and tightened
the consumer-substitution rule. The factory-pattern shape is the natural
next codification — same lesson (layers without capability are
speculative coupling), one tier up in abstraction.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs(readme): drop stale Clean Architecture refs + trim verbose preamble

PR #31 collapsed CatalogService from Clean Architecture to VSA, but the
README still referenced CatalogService as a "deliberate Clean Architecture
carve-out" and described the design as "mixed per-service architecture."
Both claims contradicted the (correct) Project Structure section that said
VSA across all five services.

Changes:
- Collapsed the four-bullet "About this repo" preamble into two bullets:
  Monorepo + single architectural shape (was three contradictory bullets
  plus a long MySQL-translation aside), and the two-database-engines
  bullet retained.
- Updated the How It Works doc description: "Clean Architecture" -> "VSA
  layout" (matches docs/how-it-works.md which was already updated in the
  collapse).
- Dropped the seven `Source: file.excalidraw` subtitle lines under the
  hero diagram + each of the six reference diagrams. The SVG is already
  embedded and clickable to full-size; the editable .excalidraw sources
  are now noted once in the section intro.

Net: -20 lines, -1 contradiction.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore(claude): apply review fixes + encode async-API / 202 Accepted rule

Two threads in one commit:

REVIEW FIXES (3) from the architecture-reviewer pass on the prior commits:

1. Reconciled categorization drift on "MISSING FACTORY at 2+ impls" between
   architecture-reviewer.md (was "Must-fix on absence when per-call selection
   is required") and .coderabbit.yaml (was "Should-consider", no qualifier).
   Both now agree: Must-fix when per-call routing is intended (latent bug —
   plain AddScoped registrations collide silently, DI returns last-registered
   impl every time, dropping routing intent); Should-consider when impls are
   interchangeable (no live bug, but tighten for explicitness).
2. Split architecture-reviewer.md's dual-category bullet into two cleaner
   bullets — one for the per-call-selection Must-fix case, one for the
   interchangeable Should-consider case.
3. Narrowed the Infrastructure DI section heading: the glob now stands alone
   (`**/Infrastructure/DependencyInjection.cs`), and the broader port-adapter
   scope is lifted into the body so the glob and the prose claim match.

NEW RULE — long-running work / 202 Accepted shape. Encoded across CLAUDE.md
"Performance Rules" + .coderabbit.yaml Endpoints path + architecture-reviewer
Endpoints section. Rule: if a write handler can take more than ~1 second,
reshape as 202 Accepted (validate + persist intent + publish Wolverine
message + return job ID with Location header). Background handler does the
work; client polls or gets pushed. NextAurora already has all the machinery
(Wolverine + Service Bus + outbox + saga) — the rule is "use it when a
handler would otherwise block on minutes-scale work." Reference shape:
POST /api/v1/orders (place → publish OrderPlaced → return OrderId; downstream
PaymentService + ShippingService handle saga via async events). Same rule
applies to Wolverine handlers themselves — if the handler body runs for
minutes, factor the work into a follow-up message handler.

Why now: the rule isn't actively violated in the codebase today (no
minutes-scale synchronous endpoints exist), but the encoding is what
guarantees future endpoints don't introduce one. Same logic as the factory
pattern rule landed in the prior commit — encode the shape before the
violation, not after.

Verified no existing "See CLAUDE.md" markers paraphrase a contradictory
version of the new rule (the two nearest markers reference money calculations
and consumer substitution, both unaffected).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore(claude): encode 3 more rules + tighten 202 Accepted + STATUS.md follow-up

Three new rules + one rule tightening + one open-issue acknowledgement,
all sourced from two Milan Jovanović / Kerim Kara articles on scaling
patterns. PR scope continues to be "encode the rules before the next
instance hits review."

Rules added:

1. NON-SARGABLE PREDICATES DEFEAT INDEXES — fix at write time.
   A Where(...) that wraps a column in a function (u.Email.ToLower() == x,
   o.CreatedAt.Date == today, EF.Functions.ILike with leading wildcard)
   defeats any B-tree index on that column. Fix at write time: normalize
   on insert/update + Where against the normalized column, or use
   case-insensitive column collation. Leading-wildcard substring search
   isn't B-tree-indexable in any database — escalate to Postgres tsvector
   full-text or a dedicated search engine when load justifies it.
   CatalogService/Features/SearchProducts.cs already documents the
   leading-wildcard trade-off explicitly; updated its cross-reference to
   point at the new rule by name.

2. FAN-OUT BELONGS ON THE MESSAGE BUS, NOT IN A SYNCHRONOUS HANDLER LOOP.
   A handler that iterates a recipient list inline and awaits a sender
   per recipient holds the request open for N × per-recipient-latency
   and concentrates work on one process. Right shape: publish one
   Wolverine message per recipient (or per batch of K), return immediately,
   let per-recipient handlers run in parallel under
   MaxDegreeOfParallelism throttle. Preventative — not retroactively
   violated today (NotificationService receives one event = one outbound
   notification).

3. PARALLELIZE INDEPENDENT AWAITS WITH TASK.WHENALL.
   Sequential await of independent I/O calls serializes latency for free.
   When a handler makes N independent I/O calls (N gRPC to different
   services, N HTTP to different external APIs, N queries against
   DIFFERENT DbContexts), Task.WhenAll pays the max latency, not the sum.
   Reference: OrderService/Features/PlaceOrder.cs:93 (gRPC fan-out over
   per-line GetProductAsync with the DbContext-safety caveat documented
   inline). Explicit caveats encoded: don't WhenAll dependent operations,
   operations sharing the same DbContext scope (not thread-safe), or
   operations whose per-call failure observability matters (WhenAll
   surfaces only the first exception).

Rule tightening (from prior architecture-reviewer feedback):

4. 202 ACCEPTED RULE — two definitional clarifications.
   (a) "Tracking row" can be the aggregate itself when its ID is the
       polling key (POST /api/v1/orders pattern), not necessarily a
       separate jobs table.
   (b) "Commit atomically" branches by dispatch path: bus.InvokeAsync
       gets AutoApplyTransactions for free; inline persist+publish needs
       the explicit BeginTransactionAsync → SaveChangesAsync → CommitAsync
       wrap from the existing Outbox-outside-handler rule. Cross-reference
       added.

STATUS.md follow-up:

5. POST /api/v1/payments/process noted as a partial Should-consider
   under the new long-running-work rule. Endpoint returns 202 but the
   handler awaits Stripe synchronously; typical sub-second but tail
   latency on degraded states is seconds-to-30s. Deferred (current
   shape works for the demo; rule is encoded preventatively).

Note on the second Kerim Kara article ("Every Senior .NET Developer Has
Defended This Architecture"): most of its lessons are already encoded in
NextAurora — the I*Repository deletion (PR #30), the CatalogService
Clean→VSA collapse (PR #31), the "Interfaces earn their keep through
consumer substitution" rule, the integration-tests-over-mocked-repository
discipline, the DbContext-direct data-access shape. The article reads
as a victory lap for the simplicity refactor. The one net-new takeaway
was the sequential-awaits trap, encoded here as rule #3.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore(claude): encode Server-side pricing rule + tighten paraphrase

Surfaced by /check-rules audit: `OrderService/Features/PlaceOrder.cs:138`
had a `See CLAUDE.md.` marker with no specific section named, paraphrasing
a principle ("never trust client-submitted prices for money calculations")
that wasn't a named rule in CLAUDE.md. Option 2 (per the audit prompt):
add the named rule, tighten the paraphrase to point at it.

Changes:
- CLAUDE.md "Security Requirements" gets a new named bullet:
  "Server-controlled fields are computed server-side, never trusted from
  the client" — covers money fields (Price/Currency/Tax), authorization
  identifiers (BuyerId/SellerId), state-machine columns (Status), and
  security flags (IsAdmin/IsDeleted). Specifies the failure mode (price
  tampering — client submits Price=0.01 for a $999 product) and the
  canonical pattern (fetch authoritative value from its source; treat
  the request DTO as untrusted input). References PlaceOrder.cs as the
  reference example. Cross-links to the existing "Mass assignment" check
  in the architecture-reviewer.
- OrderService/Features/PlaceOrder.cs:138 cross-reference tightened from
  generic `See CLAUDE.md.` to the named rule.

No other files paraphrase this principle today (verified via the prior
/check-rules audit run that found this single drift).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
@emeraldleaf emeraldleaf deleted the refactor/simplicity-drop-repos-integration-tests branch June 4, 2026 00:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants