Skip to content

Full codebase review - #8

Merged
imadnan4 merged 5 commits into
mainfrom
review/full-codebase
Jul 25, 2026
Merged

Full codebase review#8
imadnan4 merged 5 commits into
mainfrom
review/full-codebase

Conversation

@imadnan4

@imadnan4 imadnan4 commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Entire InventoryFlow codebase for CodeRabbit review.

Summary by CodeRabbit

  • New Features
    • Added backend API with browser-based refresh-cookie sessions, workspace switching, and owner/member collaboration invitations.
    • Introduced workspace-scoped catalog and operational endpoints (products, suppliers, warehouses, inventory receipts/issues & balances, purchase receipts, sales fulfillments, and warehouse transfers) with idempotency and tenancy isolation.
  • Documentation
    • Updated local/production setup guidance, including JWT/cookie configuration, CORS expectations, and refresh-cookie behavior.
  • Tests
    • Added unit and integration coverage for authentication, tenancy/workspace resolution, collaboration, catalogs, inventory ledger rules, error handling, and concurrency/idempotency scenarios.

Copilot AI review requested due to automatic review settings July 23, 2026 18:28

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.

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ee14eb71-3bdc-434c-ba17-01f35f047434

📥 Commits

Reviewing files that changed from the base of the PR and between c9eab4e and b50f6c5.

📒 Files selected for processing (2)
  • backend/src/InventoryFlow.Infrastructure/Purchases/EfPurchaseReceiptService.cs
  • frontend/src/features/warehouses/pages/WarehousesPage.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/src/InventoryFlow.Infrastructure/Purchases/EfPurchaseReceiptService.cs

📝 Walkthrough

Walkthrough

This PR introduces a complete Inventory Flow application: a .NET 9 backend (Domain/Application/Infrastructure/Api) with JWT authentication, workspace tenancy, EF Core migrations, and inventory/product/supplier/warehouse/purchase/sales/transfer features; a React/Vite frontend consuming these APIs; Docker/compose deployment; and unit/integration test suites.

Changes

Backend (.NET API)

Layer / File(s) Summary
Domain model
backend/src/InventoryFlow.Domain/...
Entities (Workspace, Product, Supplier, Warehouse, InventoryMovement/Balance, PurchaseReceipt, SalesFulfillment, WarehouseTransfer, RefreshToken), roles, DomainException.
Application layer: auth & collaboration
backend/src/InventoryFlow.Application/Features/Authentication/..., .../Collaboration/...
MediatR commands/queries/handlers/validators for register/login/refresh/logout/switch-workspace and invitation lifecycle.
Application layer: catalog & inventory ops
backend/src/InventoryFlow.Application/Features/{Products,Suppliers,Warehouses,Inventory,Purchases,Sales,Transfers}/...
Commands/queries/handlers/validators for products, suppliers, warehouses, inventory movements/balances, purchase receipts, sales fulfillments, transfers.
Infrastructure: auth & tenancy
backend/src/InventoryFlow.Infrastructure/Authentication/..., Collaboration/EfCollaborationService.cs, DependencyInjection/..., Identity/ApplicationUser.cs, Tenancy/CurrentWorkspaceResolver.cs
IdentityAuthenticationService, JWT issuer/options, refresh token generation, DI wiring, workspace claim resolution.
Infrastructure: EF catalogs & ledger
backend/src/InventoryFlow.Infrastructure/{Products,Suppliers,Warehouses,Inventory,Purchases,Sales,Transfers}/...
EF-backed services with idempotency, unique-constraint conflict mapping, and atomic balance updates.
Persistence: DbContext, configs, migrations
backend/src/InventoryFlow.Infrastructure/Persistence/...
ApplicationDbContext, entity configurations, and the full migration sequence building identity/workspace/inventory schema.
API controllers & bootstrap
backend/src/InventoryFlow.Api/...
Controllers for auth/collaboration/products/suppliers/warehouses/inventory/purchases/sales/transfers, GlobalExceptionHandler, Program.cs, appsettings.
Backend build config
backend/*.sln, Directory.*.props, Dockerfile, .dockerignore, README.md
Solution/build/Docker configuration.
Backend tests
backend/tests/...
Unit tests for domain invariants; integration tests for auth, collaboration, inventory, products, purchases, sales, suppliers, warehouses, transfers, tenancy.

Frontend (React/Vite)

Layer / File(s) Summary
Build/tooling config
frontend/package.json, vite.config.ts, tsconfig*.json, eslint.config.js, Dockerfile, nginx.conf
Project scaffolding and build tooling.
App shell
frontend/src/App.tsx, app/..., lib/..., store/..., index.css, layouts/DashboardLayout.tsx
Providers, router, API client with token refresh, query client, UI/auth stores.
Auth feature
frontend/src/features/auth/...
Login/register pages, route guards, session bootstrap, auth API/schema.
UI component library
frontend/src/components/...
Layout (Sidebar/Topbar), theme provider, base-ui component primitives.
Dashboard
frontend/src/features/dashboard/...
Metric cards and dashboard page with charts.
Feature pages
frontend/src/features/{products,inventory,purchases,sales,suppliers,warehouses,transfers,reports}/...
CRUD pages, API clients, Zod schemas, and types for each domain feature.

Deployment tooling

Layer / File(s) Summary
Compose & SDK pinning
docker-compose.yml, global.json, mise.toml, .env.example
SQL Server/migrator/api/web orchestration and .NET SDK version pinning.

Estimated code review effort: 5 (Critical) | ~180 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant ApiClient as Frontend apiClient
  participant AuthController
  participant IdentityAuthenticationService
  participant Db as ApplicationDbContext

  Browser->>ApiClient: login(email, password)
  ApiClient->>AuthController: POST /api/auth/login
  AuthController->>IdentityAuthenticationService: LoginAsync(command)
  IdentityAuthenticationService->>Db: validate user, resolve workspace
  Db-->>IdentityAuthenticationService: user + workspace
  IdentityAuthenticationService-->>AuthController: AuthenticationSession
  AuthController-->>ApiClient: 200 OK + refresh cookie + access token
  ApiClient-->>Browser: store session, redirect to dashboard
Loading
sequenceDiagram
  participant Browser
  participant InventoryController
  participant RecordReceiptHandler
  participant EfInventoryLedger
  participant Db as ApplicationDbContext

  Browser->>InventoryController: POST /api/inventory/receipts (Idempotency-Key)
  InventoryController->>RecordReceiptHandler: RecordReceiptCommand
  RecordReceiptHandler->>EfInventoryLedger: RecordAsync(...)
  EfInventoryLedger->>Db: begin Serializable transaction
  EfInventoryLedger->>Db: check idempotency key, upsert InventoryBalance
  Db-->>EfInventoryLedger: InventoryMovement
  EfInventoryLedger-->>RecordReceiptHandler: InventoryMovement
  RecordReceiptHandler-->>InventoryController: InventoryMovementResponse
  InventoryController-->>Browser: 201 Created
Loading

Possibly related PRs

  • imadnan4/inventory-flow#1: Also modifies GlobalExceptionHandler.cs to map domain exceptions into RFC 7807 ProblemDetails responses.

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 56.12% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is too generic and doesn't describe the main change in the changeset. Use a concise title that names the primary scope, such as adding the InventoryFlow backend, frontend, and integration test suite.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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.
✨ 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 review/full-codebase

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

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

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (24)
backend/src/InventoryFlow.Api/Program.cs-23-33 (1)

23-33: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Partition the "api" rate limit by caller.

This is a single shared bucket, so one caller can consume the entire 100 requests/minute budget and throttle every other request, including /health. Use a partitioned policy keyed by authenticated subject or client IP instead.

🤖 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 `@backend/src/InventoryFlow.Api/Program.cs` around lines 23 - 33, Replace the
shared fixed-window limiter registered in AddRateLimiter with a partitioned
policy for the "api" limiter. Key each caller’s bucket by authenticated subject
when available, otherwise by client IP, while preserving the 100
requests-per-minute window, queue settings, and 429 rejection status.
backend/src/InventoryFlow.Api/ExceptionHandling/GlobalExceptionHandler.cs-80-82 (1)

80-82: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

409-mapped conflict exceptions don't expose Detail.

ProductSkuConflictException, SupplierNameConflictException, WarehouseNameConflictException, and CollaborationConflictException all map to 409 (lines 45-50), but only InsufficientInventoryException/InventoryArchiveConflictException (and DomainException) get their message surfaced via Detail. Clients hitting a duplicate-name/SKU conflict get a generic "conflicting value exists" body with no explanation.

🩹 Proposed fix: include all conflict exceptions
-            Detail = exception is DomainException or InsufficientInventoryException or InventoryArchiveConflictException
-                ? exception.Message
-                : null,
+            Detail = exception is DomainException or InsufficientInventoryException or InventoryArchiveConflictException
+                or ProductSkuConflictException or SupplierNameConflictException or WarehouseNameConflictException
+                or CollaborationConflictException
+                ? exception.Message
+                : null,
🤖 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 `@backend/src/InventoryFlow.Api/ExceptionHandling/GlobalExceptionHandler.cs`
around lines 80 - 82, Update the Detail selection in GlobalExceptionHandler so
ProductSkuConflictException, SupplierNameConflictException,
WarehouseNameConflictException, and CollaborationConflictException also expose
exception.Message, alongside the existing DomainException,
InsufficientInventoryException, and InventoryArchiveConflictException handling.
backend/src/InventoryFlow.Api/ExceptionHandling/GlobalExceptionHandler.cs-39-58 (1)

39-58: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

All exceptions logged at Error level, including expected 4xx business errors.

logger.LogError (line 54) fires unconditionally for every exception — ValidationException, DomainException, conflict exceptions, auth failures — not just true 500s. In production this drowns real server errors in noise and undermines error-rate alerting.

🩹 Proposed fix: log level by severity
-        logger.LogError(
-            exception,
-            "Unhandled exception while processing {RequestMethod} {RequestPath}",
-            httpContext.Request.Method,
-            httpContext.Request.Path);
+        if (statusCode >= StatusCodes.Status500InternalServerError)
+        {
+            logger.LogError(exception, "Unhandled exception while processing {RequestMethod} {RequestPath}",
+                httpContext.Request.Method, httpContext.Request.Path);
+        }
+        else
+        {
+            logger.LogWarning(exception, "Request failed with {StatusCode} while processing {RequestMethod} {RequestPath}",
+                statusCode, httpContext.Request.Method, httpContext.Request.Path);
+        }
🤖 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 `@backend/src/InventoryFlow.Api/ExceptionHandling/GlobalExceptionHandler.cs`
around lines 39 - 58, Update the exception logging in the global handler to
select the log level based on the computed statusCode: retain Error for
500-level unhandled exceptions and use a lower appropriate severity for expected
4xx validation, authorization, authentication, domain, and conflict exceptions.
Preserve the existing status mapping and request context, and ensure every
exception is still logged.
frontend/.gitignore-10-13 (1)

10-13: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Ignore environment files in Git.

.env and .env.production remain trackable, so deployment credentials or other configuration can be committed. Mirror the .dockerignore rule while retaining the example file.

Proposed fix
 node_modules
 dist
 dist-ssr
 *.local
+.env
+.env.*
+!.env.example
🤖 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 `@frontend/.gitignore` around lines 10 - 13, Update the frontend ignore rules
to exclude environment files, including .env and .env.production, while
explicitly retaining the example environment file as tracked. Mirror the
existing environment-file pattern used by .dockerignore.
frontend/src/lib/api-client.ts-46-60 (1)

46-60: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Prevent pre-logout requests from refreshing a new session.

A request sent before invalidateSession() can return 401 afterward. Since Line 57 captures the post-logout epoch, it may refresh with a still-valid cookie and call setSession(), resurrecting the session while logout is in flight. Stamp each request with its originating epoch and reject its 401 when it no longer matches sessionEpoch.

Proposed fix
+type AuthRequestConfig = InternalAxiosRequestConfig & {
+  _authRetried?: boolean
+  _sessionEpoch?: number
+}
+
 apiClient.interceptors.request.use((config) => {
+  const request = config as AuthRequestConfig
+  request._sessionEpoch = sessionEpoch
   const token = useAuthStore.getState().accessToken
-  if (token) config.headers.Authorization = `Bearer ${token}`
-  return config
+  if (token) request.headers.Authorization = `Bearer ${token}`
+  return request
 })
 apiClient.interceptors.response.use(undefined, async (error: AxiosError) => {
-  const request = error.config as
-    (InternalAxiosRequestConfig & { _authRetried?: boolean }) | undefined
-  if (error.response?.status !== 401 || !request || request._authRetried)
+  const request = error.config as AuthRequestConfig | undefined
+  if (
+    error.response?.status !== 401 ||
+    !request ||
+    request._authRetried ||
+    request._sessionEpoch !== sessionEpoch
+  )
     return Promise.reject(error)
🤖 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 `@frontend/src/lib/api-client.ts` around lines 46 - 60, Update the request and
response interceptors around refreshAccessToken to stamp each request with its
originating sessionEpoch, then reject 401 responses when that stamp no longer
matches the current sessionEpoch. Ensure the epoch is captured before the
request is sent and checked before refreshing or calling setSession, preventing
invalidateSession from being undone by an in-flight request.
backend/src/InventoryFlow.Domain/Entities/PurchaseReceipt.cs-9-21 (1)

9-21: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Reject null idempotency keys at the domain boundary.

idempotencyKey is passed to NormalizeIdempotencyKey without an explicit null check. Runtime callers can bypass nullable annotations and trigger an unhandled exception instead of a domain validation failure.

As per path instructions, missing null checks in **/*.cs are HIGH priority.

Proposed fix
     {
         if (id == Guid.Empty || workspaceId == Guid.Empty || supplierId == Guid.Empty || warehouseId == Guid.Empty ||
             productId == Guid.Empty || inventoryMovementId == Guid.Empty)
             throw new DomainException("Purchase receipt identifiers are required.");
         if (receivedAtUtc.Offset != TimeSpan.Zero) throw new DomainException("Purchase receipt time must be in UTC.");
+        if (idempotencyKey is null) throw new DomainException("Purchase receipt idempotency key is required.");
         WorkspaceId = workspaceId;
🤖 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 `@backend/src/InventoryFlow.Domain/Entities/PurchaseReceipt.cs` around lines 9
- 21, Update the PurchaseReceipt constructor to explicitly reject a null
idempotencyKey before calling InventoryMovement.NormalizeIdempotencyKey, raising
the appropriate DomainException for invalid input while preserving existing
normalization for non-null keys.

Source: Path instructions

backend/src/InventoryFlow.Application/Features/Authentication/AuthenticationValidators.cs-11-11 (1)

11-11: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Make the display-name predicate null-safe in AuthenticationValidators.cs:11. name.Trim() can throw on null input and turn validation into an exception instead of a normal error. Guard the predicate itself or stop the rule chain before Must.

🤖 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
`@backend/src/InventoryFlow.Application/Features/Authentication/AuthenticationValidators.cs`
at line 11, Update the DisplayName rule in AuthenticationValidators so its
whitespace predicate is null-safe and never calls Trim on a null value. Guard
the Must predicate or stop validation before it executes, while preserving the
existing required-field and whitespace validation messages.

Source: Path instructions

backend/src/InventoryFlow.Application/Common/Tenancy/CurrentWorkspace.cs-6-6 (1)

6-6: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard Name against runtime null values.

string Name can still receive null from another implementation or deserialization via null!, allowing an invalid workspace context to propagate. Validate it in an explicit constructor before assigning the property. As per path instructions, **/*.cs: “Flag missing null checks as HIGH.”

🤖 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 `@backend/src/InventoryFlow.Application/Common/Tenancy/CurrentWorkspace.cs` at
line 6, Update the CurrentWorkspace record to use an explicit constructor that
validates the Name argument before assigning it, rejecting runtime null values
while preserving the existing Id, Name, and Role members and record semantics.

Source: Path instructions

frontend/src/components/layout/Topbar.tsx-45-53 (1)

45-53: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add catch blocks to async handlers.

signOut and handleWorkspaceSwitch only use try/finally — if logout() or switchWorkspace() reject, the error is unhandled (unhandled promise rejection) and the user gets no feedback (e.g. workspace switch silently fails with no toast/error state).

🔧 Proposed fix
   const signOut = async () => {
     invalidateSession()

     try {
       await logout()
+    } catch (error) {
+      console.error("Failed to log out on the server", error)
     } finally {
       navigate("/login", { replace: true })
     }
   }
   const handleWorkspaceSwitch = async (workspaceId: string) => {
     if (workspaceId === user?.workspace.id || switchingWorkspaceId) return
     setSwitchingWorkspaceId(workspaceId)

     try {
       const session = await switchWorkspace(workspaceId)
       setSession(session)
+    } catch (error) {
+      console.error("Failed to switch workspace", error)
+      toast.error("Failed to switch workspace")
     } finally {
       setSwitchingWorkspaceId(null)
     }
   }

As per path instructions, "Flag async/await without try-catch as MEDIUM."

Also applies to: 59-69

🤖 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 `@frontend/src/components/layout/Topbar.tsx` around lines 45 - 53, Update the
async handlers signOut and handleWorkspaceSwitch to add catch blocks around
logout() and switchWorkspace() failures. Handle the rejected errors through the
component’s existing user-feedback mechanism, such as the current toast or error
state, while preserving the existing cleanup and navigation behavior in finally.

Source: Path instructions

frontend/src/features/warehouses/pages/WarehousesPage.tsx-90-100 (1)

90-100: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Duplicate name field — second warehouse attribute is never captured.

Both the catalog list item and the "Add warehouse" form duplicate the same name field instead of showing/capturing a distinct second attribute:

  • Lines 96 & 98 both render {warehouse.name} in the list item.
  • Lines 121-136 render two separate <input name="name"> fields (the second label even reads lowercase "name"), so Object.fromEntries(form) collapses them into a single name key — whatever second field (e.g. code/location) this form was meant to submit is silently dropped before it ever reaches warehouseSchema.safeParse.

This looks like a copy-paste leftover from the two-field template used in ProductsPage.tsx (name + sku) that wasn't updated for warehouses.

🐛 Proposed fix (adjust to the actual intended second field)
                     <div>
                       <p className="font-medium">{warehouse.name}</p>
                       <p className="text-sm text-muted-foreground">
-                        {warehouse.name}
+                        {warehouse.code}
                       </p>
                     </div>
               <label className="block text-sm">
-                name
+                Code
                 <input
                   className="mt-1 w-full rounded-md border bg-background p-2"
-                  name="name"
+                  name="code"
                   required
                 />
               </label>

Also applies to: 121-136

🤖 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 `@frontend/src/features/warehouses/pages/WarehousesPage.tsx` around lines 90 -
100, Replace the duplicated warehouse.name rendering in the warehouse list and
the second name input in the add-warehouse form with the intended distinct
warehouse attribute, using the corresponding property, label, and form field
name consistently. Update the related submission/schema handling so
Object.fromEntries preserves that second field and warehouseSchema.safeParse
receives both warehouse values.
frontend/src/features/warehouses/pages/WarehousesPage.tsx-55-63 (1)

55-63: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Uncontrolled inputs reset on failed submit

<form action={submit}> clears these uncontrolled fields as soon as the action returns, even when safeParse fails or create.mutate rejects. Use mutateAsync/await (or switch to onSubmit/controlled inputs) if the entered values should stay visible after an error.

🤖 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 `@frontend/src/features/warehouses/pages/WarehousesPage.tsx` around lines 55 -
63, Update the submit function around warehouseSchema.safeParse and
create.mutate so the form action does not clear uncontrolled inputs when
validation or mutation fails. Use an async mutation flow with mutateAsync and
await its result, preserving the existing validation error handling while
keeping entered values visible after failures.
frontend/src/features/products/pages/ProductsPage.tsx-55-63 (1)

55-63: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep the form pending until the mutation settles.

submit returns synchronously here, so React 19 clears the uncontrolled name/sku inputs as soon as the action finishes — even on validation failures or async mutation errors. Switch this to an async action (mutateAsync/await) or onSubmit so failed submits keep the typed values. Same root cause in frontend/src/features/warehouses/pages/WarehousesPage.tsx.

🤖 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 `@frontend/src/features/products/pages/ProductsPage.tsx` around lines 55 - 63,
Update the submit handlers in ProductsPage and WarehousesPage to remain pending
until validation and the asynchronous mutation settle. Use an async action with
mutateAsync/await or move submission to onSubmit, ensuring validation failures
and mutation errors preserve the uncontrolled name/sku inputs while successful
submissions retain current behavior.
frontend/src/features/purchases/pages/PurchasesPage.tsx-1-1 (1)

1-1: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

"Retry" button can silently resubmit stale form data. In both pages, the retry payload is snapshotted once inside onError and shown via a "Retry" button, but none of the field onChange handlers clear or refresh that snapshot — so if the user edits any select/input after a failed submit, clicking "Retry" resends the old values, not what's currently visible in the form.

  • frontend/src/features/purchases/pages/PurchasesPage.tsx#L77-80: clear retryReceipt (or recompute it) whenever supplierId/warehouseId/productId/quantity change after an error, so the button always reflects current form state (or hide it once inputs diverge from the stored snapshot).
  • frontend/src/features/purchases/pages/PurchasesPage.tsx#L193-202: guard the "Retry receipt" button so it only renders/fires when retryReceipt still matches the current form values.
  • frontend/src/features/sales/pages/SalesPage.tsx#L70-73: apply the same fix to retryFulfillment.
  • frontend/src/features/sales/pages/SalesPage.tsx#L165-174: apply the same guard to the "Retry fulfillment" button.
🤖 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 `@frontend/src/features/purchases/pages/PurchasesPage.tsx` at line 1, Update
the retry state and button logic in PurchasesPage and SalesPage so editing any
form field after an error invalidates the stored retry payload, or prevents
retry unless it still matches the current values. Cover all listed supplier,
warehouse, product, quantity, and sales fulfillment fields in their onChange
handlers, and guard the respective retry buttons and callbacks using the current
form state.
frontend/src/features/suppliers/pages/SuppliersPage.tsx-55-63 (1)

55-63: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Form clears the name input immediately on submit, even when the create fails.

submit is a plain synchronous function passed to <form action={submit}>; it fires create.mutate(...) without awaiting it. React 19 automatically resets uncontrolled form fields once the action function returns — which for a sync function happens immediately, not after the async mutation settles. So the name field is cleared right away regardless of whether createSupplier later succeeds or fails, leaving the user with an error message but no way to see/correct what they typed.

🛠️ Suggested fix — make the action await the mutation so React only resets on actual completion
-  const submit = (form: FormData) => {
+  const submit = async (form: FormData) => {
     const parsed = supplierSchema.safeParse(Object.fromEntries(form))
     if (!parsed.success)
       return setError(
         parsed.error.issues[0]?.message ?? "Check the supplier details."
       )
     setError("")
-    create.mutate(parsed.data)
+    await create.mutateAsync(parsed.data).catch(() => {})
   }

Note this still resets on failure per React 19 semantics; if you want the input preserved on error, prefer a controlled input tied to state instead of relying on the native form reset.

Also applies to: 115-123

🤖 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 `@frontend/src/features/suppliers/pages/SuppliersPage.tsx` around lines 55 -
63, Update the submit function to be asynchronous and await the create.mutate
operation before returning, so the React 19 form action remains pending until
the mutation settles. Preserve the existing validation and error handling, and
update the mutation invocation in submit rather than unrelated form fields or
components.

Source: Path instructions

backend/src/InventoryFlow.Infrastructure/Persistence/Configurations/InventoryBalanceConfiguration.cs-16-18 (1)

16-18: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Enforce same-workspace foreign keys for inventory balances.

The balance carries WorkspaceId, but the warehouse and product relationships constrain only WarehouseId and ProductId. This permits a row to combine one workspace with a warehouse or product from another workspace, undermining tenant isolation if a writer or query trusts the persisted relationship.

Add composite alternate keys/foreign keys on (WorkspaceId, Id) for workspace-scoped warehouses and products, or enforce the ownership check atomically before saving and reflect that constraint in the migration.

🤖 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
`@backend/src/InventoryFlow.Infrastructure/Persistence/Configurations/InventoryBalanceConfiguration.cs`
around lines 16 - 18, Update InventoryBalanceConfiguration to enforce workspace
ownership atomically: configure composite alternate keys on Warehouse and
Product using (WorkspaceId, Id), then map the balance relationships with
composite foreign keys (WorkspaceId, WarehouseId) and (WorkspaceId, ProductId),
preserving the existing delete behaviors. Ensure the corresponding database
migration reflects these composite constraints.
backend/src/InventoryFlow.Infrastructure/Persistence/Configurations/WorkspaceInvitationConfiguration.cs-25-27 (1)

25-27: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Do not let expired invitations occupy the active unique slot.

The filtered index treats an invitation as active whenever AcceptedAtUtc and RevokedAtUtc are null, even after ExpiresAtUtc has passed. A resend to the same workspace/email can therefore fail with a unique-key violation until the old invitation is explicitly revoked or accepted.

Use a persisted active/status state that is updated when invitations expire, or make expiry cleanup and insertion atomic. Avoid relying on a moving GETUTCDATE() predicate in a SQL Server filtered index.

🤖 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
`@backend/src/InventoryFlow.Infrastructure/Persistence/Configurations/WorkspaceInvitationConfiguration.cs`
around lines 25 - 27, Update the unique filtered index in
WorkspaceInvitationConfiguration so expired invitations no longer occupy the
active workspace/email slot. Use a persisted active or status state maintained
when invitations expire, or coordinate expiry cleanup atomically with insertion;
do not use a moving GETUTCDATE() predicate in the SQL Server filter.
backend/src/InventoryFlow.Infrastructure/Persistence/Configurations/InventoryMovementConfiguration.cs-11-12 (1)

11-12: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add required null checks to each public Configure method.

  • backend/src/InventoryFlow.Infrastructure/Persistence/Configurations/InventoryMovementConfiguration.cs#L11-L12: validate builder before configuring the entity.
  • backend/src/InventoryFlow.Infrastructure/Persistence/Configurations/ProductConfiguration.cs#L11-L12: validate builder before configuring the entity.
  • backend/src/InventoryFlow.Infrastructure/Persistence/Configurations/PurchaseReceiptConfiguration.cs#L9-L10: validate builder before configuring the entity.
  • backend/src/InventoryFlow.Infrastructure/Persistence/Configurations/RefreshTokenConfiguration.cs#L14-L15: validate builder before configuring the entity.
  • backend/src/InventoryFlow.Infrastructure/Persistence/Configurations/SalesFulfillmentConfiguration.cs#L9-L10: validate builder before configuring the entity.
  • backend/src/InventoryFlow.Infrastructure/Persistence/Configurations/SupplierConfiguration.cs#L11-L12: validate builder before configuring the entity.
  • backend/src/InventoryFlow.Infrastructure/Persistence/Configurations/WarehouseConfiguration.cs#L4-L4: validate b before configuring the entity.
  • backend/src/InventoryFlow.Infrastructure/Persistence/Configurations/WarehouseTransferConfiguration.cs#L9-L10: validate builder before configuring the entity.
  • backend/src/InventoryFlow.Infrastructure/Persistence/Configurations/WorkspaceConfiguration.cs#L11-L12: validate builder before configuring the entity.

Use System.ArgumentNullException.ThrowIfNull(builder); at each method entry. As per path instructions, "**/*.cs: Flag missing null checks as HIGH."

🤖 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
`@backend/src/InventoryFlow.Infrastructure/Persistence/Configurations/InventoryMovementConfiguration.cs`
around lines 11 - 12, Each public Configure method must validate its entity
builder at method entry before applying configuration. Add
ArgumentNullException.ThrowIfNull(builder) in InventoryMovementConfiguration.cs,
ProductConfiguration.cs, PurchaseReceiptConfiguration.cs,
RefreshTokenConfiguration.cs, SalesFulfillmentConfiguration.cs,
SupplierConfiguration.cs, WarehouseTransferConfiguration.cs, and
WorkspaceConfiguration.cs; in WarehouseConfiguration.cs, validate the parameter
named b instead. Apply the same guard to all listed sites without changing the
remaining configuration logic.

Source: Path instructions

backend/src/InventoryFlow.Infrastructure/Persistence/Migrations/20260718122029_AddInventoryLedger.cs-26-80 (1)

26-80: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Enforce workspace consistency in ledger foreign keys.

These FKs validate ProductId and WarehouseId independently, so a ledger row can use WorkspaceId A with a product or warehouse owned by workspace B. This breaks the tenancy boundary and can corrupt balances/movements. Add composite principal keys and foreign keys for (WorkspaceId, ProductId) and (WorkspaceId, WarehouseId), update the EF configurations, then regenerate the migrations.

🤖 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
`@backend/src/InventoryFlow.Infrastructure/Persistence/Migrations/20260718122029_AddInventoryLedger.cs`
around lines 26 - 80, Update the ledger entity configurations and migration for
InventoryBalances and InventoryMovements so ProductId and WarehouseId are
constrained together with WorkspaceId via composite principal keys and composite
foreign keys. Add the corresponding composite alternate keys on Products and
Warehouses, remove the independent product/warehouse foreign keys, and
regenerate the migration so cross-workspace ledger references are rejected.
backend/src/InventoryFlow.Infrastructure/Persistence/Migrations/20260719073620_AddCollaborationFoundation.cs-116-121 (1)

116-121: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Allow invitations to be reissued after expiry.

This index continues to reserve (WorkspaceId, NormalizedEmail) after ExpiresAtUtc passes. EfCollaborationService.CreateInvitationAsync then returns a duplicate conflict instead of creating a replacement, although expiry makes the invitation non-pending. Atomically revoke/replace expired invitations before inserting, while retaining concurrency-safe uniqueness, then regenerate this migration/model.

🤖 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
`@backend/src/InventoryFlow.Infrastructure/Persistence/Migrations/20260719073620_AddCollaborationFoundation.cs`
around lines 116 - 121, Update EfCollaborationService.CreateInvitationAsync and
the WorkspaceInvitations uniqueness configuration so expired invitations no
longer block reissuance: atomically revoke or replace expired records before
inserting the new invitation, while preserving concurrency-safe uniqueness for
active, non-revoked invitations. Regenerate the corresponding EF migration and
model so the filtered index reflects the pending-state rules, including
ExpiresAtUtc.
backend/tests/InventoryFlow.IntegrationTests/Api/InventoryEndpointsTests.cs-200-213 (1)

200-213: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Missing null check before deserializing responses.

PostAsync<T> and RegisterSessionAsync null-forgive (!) the deserialized body without asserting it's non-null. A malformed/empty response body will throw a raw NullReferenceException instead of a clear assertion failure, making CI failures harder to diagnose — unlike the Assert.IsType<T>(...) pattern already used elsewhere in this test suite (e.g., CollaborationEndpointsTests.cs).

✅ Suggested fix
-        return (await response.Content.ReadFromJsonAsync<T>())!;
+        var payload = await response.Content.ReadFromJsonAsync<T>();
+        return Assert.IsType<T>(payload);

Apply the same pattern to RegisterSessionAsync (line 212).

As per path instructions, "Flag missing null checks as HIGH."

🤖 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 `@backend/tests/InventoryFlow.IntegrationTests/Api/InventoryEndpointsTests.cs`
around lines 200 - 213, HIGH: Add explicit non-null assertions for deserialized
response bodies in PostAsync<T> and RegisterSessionAsync, replacing the
null-forgiving operators with the test suite’s established assertion pattern
(such as Assert.IsType<T>(...)); preserve the existing status-code checks and
return the validated deserialized values.

Source: Path instructions

backend/tests/InventoryFlow.IntegrationTests/Api/AuthenticatedApiFixture.cs-39-69 (1)

39-69: 🩺 Stability & Availability | 🟠 Major | ⚖️ Poor tradeoff

No cancellation/timeout on container startup and migration.

InitializeAsync awaits _sqlServer.StartAsync(), database creation, and MigrateAsync() with no CancellationToken or timeout. A stuck container pull/start or a hung migration will block the test run indefinitely rather than failing fast.

🕒 Suggested fix
-    public async Task InitializeAsync()
+    public async Task InitializeAsync(CancellationToken cancellationToken = default)
     {
-        await _sqlServer.StartAsync();
+        using var timeoutCts = new CancellationTokenSource(TimeSpan.FromMinutes(2));
+        using var linked = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
+        await _sqlServer.StartAsync(linked.Token);

As per path instructions, "Flag async/await without proper cancellation token handling as MEDIUM."

🤖 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 `@backend/tests/InventoryFlow.IntegrationTests/Api/AuthenticatedApiFixture.cs`
around lines 39 - 69, Update AuthenticatedApiFixture.InitializeAsync to apply a
bounded cancellation timeout across _sqlServer.StartAsync, database creation,
and ApplicationDbContext.Database.MigrateAsync. Create and manage a
CancellationTokenSource for the initialization scope, pass its token to each
supported async operation, and ensure timeout cancellation causes the test setup
to fail promptly rather than waiting indefinitely.

Source: Path instructions

backend/src/InventoryFlow.Infrastructure/Products/EfProductCatalog.cs-42-65 (1)

42-65: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make the archive retry state-safe.

ArchiveAsync reuses the same dbContext across retries. Because Product.Archive() is idempotent, a retry can keep the tracked product marked archived and return true even if the prior transaction rolled back, leaving the row unarchived. Use a fresh context per attempt or a transaction helper that verifies commit success. Also pass cancellationToken into ExecuteAsync(...).

🤖 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 `@backend/src/InventoryFlow.Infrastructure/Products/EfProductCatalog.cs` around
lines 42 - 65, Update ArchiveAsync to make each execution-strategy retry
state-safe by creating and using a fresh DbContext within every attempt, rather
than reusing the tracked context and product across retries. Pass
cancellationToken to the strategy’s ExecuteAsync call, and preserve the existing
archive, conflict, rollback, and success behavior while ensuring true is
returned only after a successful commit.

Sources: Path instructions, MCP tools

backend/src/InventoryFlow.Infrastructure/Inventory/InventoryLedgerWriter.cs-17-19 (1)

17-19: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Idempotency short-circuits match only on the idempotency key, never validating the rest of the request. All four write paths look up an existing record by WorkspaceId + IdempotencyKey alone and return it immediately on a hit, without comparing the other request parameters (warehouse/product/type/quantity, supplier, or source/destination). A key reused with different parameters silently returns the stale record instead of surfacing a conflict, which can mask client bugs or hide a real business-logic mismatch behind an apparently-successful response.

  • backend/src/InventoryFlow.Infrastructure/Inventory/InventoryLedgerWriter.cs#L17-L19: after finding existing, compare its WarehouseId/ProductId/Type/Quantity to the request and throw a conflict (e.g., a dedicated IdempotencyConflictException) on mismatch instead of returning existing unconditionally.
  • backend/src/InventoryFlow.Infrastructure/Purchases/EfPurchaseReceiptService.cs#L22-L28: same — validate existing.SupplierId/WarehouseId/ProductId/Quantity against the request before returning it.
  • backend/src/InventoryFlow.Infrastructure/Sales/EfSalesFulfillmentService.cs#L22-L28: same — validate existing.WarehouseId/ProductId/Quantity against the request before returning it.
  • backend/src/InventoryFlow.Infrastructure/Transfers/EfWarehouseTransferService.cs#L42-L48: same — validate existing.SourceWarehouseId/DestinationWarehouseId/ProductId/Quantity against the request before returning it.
🤖 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 `@backend/src/InventoryFlow.Infrastructure/Inventory/InventoryLedgerWriter.cs`
around lines 17 - 19, Idempotency hits must reject reused keys with different
request parameters instead of returning stale records. In
InventoryLedgerWriter.cs lines 17-19, compare WarehouseId, ProductId, Type, and
Quantity before returning existing; in EfPurchaseReceiptService.cs lines 22-28,
compare SupplierId, WarehouseId, ProductId, and Quantity; in
EfSalesFulfillmentService.cs lines 22-28, compare WarehouseId, ProductId, and
Quantity; and in EfWarehouseTransferService.cs lines 42-48, compare
SourceWarehouseId, DestinationWarehouseId, ProductId, and Quantity. Throw a
dedicated idempotency conflict exception on any mismatch, otherwise return the
existing record.
backend/src/InventoryFlow.Infrastructure/Inventory/EfInventoryLedger.cs-17-27 (1)

17-27: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Create a fresh ApplicationDbContext per ExecuteAsync attempt

  • backend/src/InventoryFlow.Infrastructure/Inventory/EfInventoryLedger.cs, backend/src/InventoryFlow.Infrastructure/Purchases/EfPurchaseReceiptService.cs, and backend/src/InventoryFlow.Infrastructure/Sales/EfSalesFulfillmentService.cs all reuse the injected context inside the retry delegate. If a transient failure happens after an entity is tracked, the next attempt replays with stale Added state; in the purchase/sales flows the new Guid.NewGuid() movement key also makes the replay capable of posting a second inventory movement and double-applying stock.
  • Mirror backend/src/InventoryFlow.Infrastructure/Transfers/EfWarehouseTransferService.cs and resolve a fresh ApplicationDbContext inside the delegate for each attempt.
🤖 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 `@backend/src/InventoryFlow.Infrastructure/Inventory/EfInventoryLedger.cs`
around lines 17 - 27, Update the retry delegates in
backend/src/InventoryFlow.Infrastructure/Inventory/EfInventoryLedger.cs (lines
17-27),
backend/src/InventoryFlow.Infrastructure/Purchases/EfPurchaseReceiptService.cs
(lines 18-19), and
backend/src/InventoryFlow.Infrastructure/Sales/EfSalesFulfillmentService.cs
(lines 18-19) to resolve a fresh ApplicationDbContext inside each ExecuteAsync
attempt, mirroring EfWarehouseTransferService. Use that per-attempt context for
transactions, writers, entity tracking, saves, and commits so retries do not
reuse stale state or generate duplicate movements.
🟡 Minor comments (10)
docker-compose.yml-20-22 (1)

20-22: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Expose the SDK pin to the backend Docker build

  • docker-compose.yml#L20-L22: build.context: ./backend excludes the repo-root global.json, so the container build falls back to the floating mcr.microsoft.com/dotnet/sdk:9.0 base in backend/Dockerfile.
  • global.json#L1-L6: "rollForward": "latestPatch" still allows patch drift; set it to "disable" if the build must stay on 9.0.316.
🤖 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 `@docker-compose.yml` around lines 20 - 22, Update docker-compose.yml build
configuration to use the repository root as the Docker build context so
backend/Dockerfile can access global.json. In global.json, change rollForward to
disable to pin the SDK to the specified 9.0.316 version; apply these changes at
docker-compose.yml lines 20-22 and global.json lines 1-6.
backend/src/InventoryFlow.Api/Controllers/InventoryController.cs-27-27 (1)

27-27: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a routable Location target for these Created(...) responses.

These responses point at detail URLs that the API does not expose:

  • backend/src/InventoryFlow.Api/Controllers/InventoryController.cs#L27
  • backend/src/InventoryFlow.Api/Controllers/InventoryController.cs#L43
  • backend/src/InventoryFlow.Api/Controllers/ProductsController.cs#L24

Add matching detail endpoints or return a Location that resolves to an existing route.

🤖 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 `@backend/src/InventoryFlow.Api/Controllers/InventoryController.cs` at line 27,
Update the Created responses in InventoryController at
backend/src/InventoryFlow.Api/Controllers/InventoryController.cs lines 27-27 and
43-43, and ProductsController at
backend/src/InventoryFlow.Api/Controllers/ProductsController.cs line 24, so each
Location targets an existing routable detail endpoint; alternatively add
matching detail actions for those URLs. Preserve the existing NotFound behavior
and response bodies.
backend/src/InventoryFlow.Api/ExceptionHandling/GlobalExceptionHandler.cs-73-79 (1)

73-79: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Missing explicit Type URI case for 401.

Status401Unauthorized isn't listed, so it falls through to the _ default, which is the RFC 9110 section for 500 Server Error (#section-15.6.1) rather than the 401 section (#section-15.5.2). Title switch (line 72) already handles 401 correctly — Type switch is inconsistent with it.

🩹 Proposed fix
             Type = statusCode switch
             {
                 StatusCodes.Status400BadRequest => "https://tools.ietf.org/html/rfc9110#section-15.5.1",
+                StatusCodes.Status401Unauthorized => "https://tools.ietf.org/html/rfc9110#section-15.5.2",
                 StatusCodes.Status403Forbidden => "https://tools.ietf.org/html/rfc9110#section-15.5.4",
                 StatusCodes.Status409Conflict => "https://tools.ietf.org/html/rfc9110#section-15.5.10",
                 _ => "https://tools.ietf.org/html/rfc9110#section-15.6.1",
             },
🤖 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 `@backend/src/InventoryFlow.Api/ExceptionHandling/GlobalExceptionHandler.cs`
around lines 73 - 79, Add an explicit StatusCodes.Status401Unauthorized case to
the Type switch in GlobalExceptionHandler, mapping it to the RFC 9110 URI for
section 15.5.2. Leave the existing mappings and default case unchanged.
backend/src/InventoryFlow.Application/Features/Authentication/AuthenticationValidators.cs-20-24 (1)

20-24: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add missing auth-scope validation. GetCurrentUserQueryValidator should also require WorkspaceId, and SwitchWorkspaceCommand needs validation for UserId, WorkspaceId, and RefreshToken.

🤖 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
`@backend/src/InventoryFlow.Application/Features/Authentication/AuthenticationValidators.cs`
around lines 20 - 24, Update GetCurrentUserQueryValidator to require both UserId
and WorkspaceId, and add or update the SwitchWorkspaceCommand validator to
require UserId, WorkspaceId, and RefreshToken using the existing NotEmpty
validation style.
frontend/src/features/dashboard/pages/DashboardPage.tsx-102-102 (1)

102-102: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make the Export report action functional or disable it.

This button has no click handler or navigation target, so it silently does nothing.

🤖 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 `@frontend/src/features/dashboard/pages/DashboardPage.tsx` at line 102, Update
the Export report Button in DashboardPage so it performs the intended
report-export action by wiring it to the existing export handler or navigation
target; if no implementation is available, render the button disabled instead of
leaving it inert.
frontend/src/features/dashboard/components/MetricCard.tsx-28-28 (1)

28-28: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Separate metric direction from favorable sentiment.

A decrease in “Low stock items” is beneficial, but this component always renders down as destructive. Add an explicit sentiment/status prop and mark this metric as positive.

  • frontend/src/features/dashboard/components/MetricCard.tsx#L28-L28: derive visual color and indicator from an explicit favorable/unfavorable value rather than direction.
  • frontend/src/features/dashboard/components/MetricCard.tsx#L46-L49: use that sentiment for the text color.
  • frontend/src/features/dashboard/pages/DashboardPage.tsx#L63-L67: mark the low-stock decrease as favorable.
🤖 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 `@frontend/src/features/dashboard/components/MetricCard.tsx` at line 28, Update
MetricCard to accept an explicit favorable/unfavorable sentiment prop, and
derive the trend indicator and text color from that sentiment instead of
assuming trend "up" is always positive. In
frontend/src/features/dashboard/components/MetricCard.tsx lines 28-28 and 46-49,
use the sentiment for indicator and color selection; in
frontend/src/features/dashboard/pages/DashboardPage.tsx lines 63-67, mark the
low-stock decrease as favorable.
backend/tests/InventoryFlow.IntegrationTests/Api/InventoryFlowApiFactory.cs-19-33 (1)

19-33: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Jwt env vars set in the constructor are never restored on Dispose.

Only _originalConnectionString is captured and restored; Jwt__SigningKey, Jwt__Issuer, and Jwt__Audience set in the constructor (lines 30-32) are left in the process environment after Dispose, unlike the more complete save/restore pattern in AuthenticatedApiFixture.cs.

♻️ Suggested fix
     private readonly string? _originalConnectionString = Environment.GetEnvironmentVariable(
         ConnectionStringEnvironmentVariable);
+    private readonly string? _originalSigningKey = Environment.GetEnvironmentVariable("Jwt__SigningKey");
+    private readonly string? _originalIssuer = Environment.GetEnvironmentVariable("Jwt__Issuer");
+    private readonly string? _originalAudience = Environment.GetEnvironmentVariable("Jwt__Audience");
...
         if (disposing)
         {
             Environment.SetEnvironmentVariable(
                 ConnectionStringEnvironmentVariable,
                 _originalConnectionString);
+            Environment.SetEnvironmentVariable("Jwt__SigningKey", _originalSigningKey);
+            Environment.SetEnvironmentVariable("Jwt__Issuer", _originalIssuer);
+            Environment.SetEnvironmentVariable("Jwt__Audience", _originalAudience);
         }

Also applies to: 67-77

🤖 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 `@backend/tests/InventoryFlow.IntegrationTests/Api/InventoryFlowApiFactory.cs`
around lines 19 - 33, Update InventoryFlowApiFactory to capture the original
Jwt__SigningKey, Jwt__Issuer, and Jwt__Audience values alongside
_originalConnectionString, then restore each original value in Dispose using the
existing environment-variable cleanup pattern from AuthenticatedApiFixture.
Preserve restoration of the connection string and ensure unset variables are
returned to an unset state.
backend/Dockerfile-1-1 (1)

1-1: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Pin the .NET image tags to the repo’s exact SDK version

global.json is locked to 9.0.316, but backend/Dockerfile uses mcr.microsoft.com/dotnet/sdk:9.0 and mcr.microsoft.com/dotnet/aspnet:9.0, so builds can drift to a different patch than the repo pin. Align both images with the pinned version, or update global.json if rolling patches are intentional.

🤖 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 `@backend/Dockerfile` at line 1, Update the Dockerfile image tags used by the
base SDK and ASP.NET runtime stages to the repository’s pinned .NET SDK version
9.0.316, keeping both stages aligned with global.json instead of using floating
9.0 tags.
backend/src/InventoryFlow.Infrastructure/Products/EfProductCatalog.cs-42-43 (1)

42-43: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Propagate cancellationToken to the execution strategy.
ExecuteAsync is called without the token, so retry delays can continue after the request is canceled. Use the cancellation-aware overload here, or ExecuteInTransactionAsync(..., cancellationToken) if you want EF to manage the transaction.

🤖 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 `@backend/src/InventoryFlow.Infrastructure/Products/EfProductCatalog.cs` around
lines 42 - 43, Update the execution-strategy call in the product catalog method
around CreateExecutionStrategy and ExecuteAsync to use the cancellation-aware
overload, passing the existing cancellationToken through retry execution and
delays. Alternatively, use ExecuteInTransactionAsync with the same token if EF
should manage the transaction.

Sources: Path instructions, MCP tools

backend/src/InventoryFlow.Application/Features/Warehouses/WarehouseValidators.cs-3-3 (1)

3-3: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Validate the normalized warehouse name length.

Warehouse.NormalizeName trims before enforcing NameMaxLength (backend/src/InventoryFlow.Domain/Entities/Warehouse.cs:4), but this validator measures the raw input. A valid 200-character canonical name with surrounding spaces is rejected before reaching the domain. Validate name.Trim().Length or normalize before validation so both layers enforce the same contract.

🤖 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
`@backend/src/InventoryFlow.Application/Features/Warehouses/WarehouseValidators.cs`
at line 3, Update CreateWarehouseCommandValidator’s Name rule to measure the
trimmed/normalized warehouse name before applying Warehouse.NameMaxLength, while
retaining the existing non-empty validation so it matches
Warehouse.NormalizeName’s contract.

…idempotency validation, session epoch, and cleanup
@imadnan4

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

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.

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 (2)
backend/src/InventoryFlow.Infrastructure/Purchases/EfPurchaseReceiptService.cs (1)

14-84: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Duplicate/orphaned code block breaks compilation.

Lines 59-83 duplicate the persistence logic already handled inside the strategy.ExecuteAsync(...) lambda (lines 24-57), but sit after the return await strategy.ExecuteAsync(...) statement closes at line 58 — making them unreachable, and they reference transaction and dbContext directly, neither of which exists in that outer scope (transaction was declared inside the lambda at line 23, scoped to attemptCtx). This is a leftover from the refactor to per-attempt scoped contexts that was never removed, and it will not compile (CS0103: The name 'transaction' does not exist in the current context).

🐛 Proposed fix: remove the orphaned tail block
                 var receipt = new PurchaseReceipt(receiptId, workspaceId, supplierId, warehouseId, productId, quantity, idempotencyKey,
                     movement.Id, receivedAtUtc);
                 attemptCtx.PurchaseReceipts.Add(receipt);
                 await attemptCtx.SaveChangesAsync(cancellationToken);
                 await transaction.CommitAsync(cancellationToken);
                 return receipt;
             });
-
-            var supplierExists = await dbContext.Suppliers.AnyAsync(supplier => supplier.Id == supplierId &&
-                supplier.WorkspaceId == workspaceId && supplier.ArchivedAtUtc == null, cancellationToken);
-            if (!supplierExists)
-            {
-                await transaction.RollbackAsync(cancellationToken);
-                return null;
-            }
-
-            var receiptId = Guid.NewGuid();
-            var movement = await new InventoryLedgerWriter(dbContext).RecordAsync(workspaceId, warehouseId, productId,
-                InventoryMovementType.Receipt, quantity, receiptId.ToString("N"), receivedAtUtc, Guid.NewGuid(), cancellationToken);
-            if (movement is null)
-            {
-                await transaction.RollbackAsync(cancellationToken);
-                return null;
-            }
-
-            var receipt = new PurchaseReceipt(receiptId, workspaceId, supplierId, warehouseId, productId, quantity, idempotencyKey,
-                movement.Id, receivedAtUtc);
-            dbContext.PurchaseReceipts.Add(receipt);
-            await dbContext.SaveChangesAsync(cancellationToken);
-            await transaction.CommitAsync(cancellationToken);
-            return receipt;
-        });
     }
🤖 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
`@backend/src/InventoryFlow.Infrastructure/Purchases/EfPurchaseReceiptService.cs`
around lines 14 - 84, Remove the orphaned persistence block after the return
from strategy.ExecuteAsync in RecordAsync, including its duplicate supplier
check, ledger write, receipt creation, save, rollback, and commit logic. Keep
the complete implementation inside the ExecuteAsync lambda unchanged, using
attemptCtx and its transaction.
frontend/src/features/warehouses/pages/WarehousesPage.tsx (1)

18-26: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use operation-specific error fallbacks.

readError is also used for the warehouse-list failure at Line 82 and archive failure at Line 52, but its fallback always says “Unable to save the warehouse.” List and archive failures therefore display misleading messages.

Proposed fix
-const readError = (error: unknown) => {
+const readError = (error: unknown, fallback = "Unable to save the warehouse.") => {
...
-  return "Unable to save the warehouse."
+  return fallback
}

Pass "Unable to load warehouses." and "Unable to archive the warehouse." at the respective call sites.

🤖 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 `@frontend/src/features/warehouses/pages/WarehousesPage.tsx` around lines 18 -
26, Update readError to accept an operation-specific fallback message, then pass
“Unable to load warehouses.” at the warehouse-list failure call site and “Unable
to archive the warehouse.” at the archive failure call site; preserve the
existing save fallback for the save operation.
🤖 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
`@backend/src/InventoryFlow.Infrastructure/Purchases/EfPurchaseReceiptService.cs`:
- Around line 19-23: Update the execution-strategy callback in
EfPurchaseReceiptService to capture the IServiceScope returned by
scopeFactory.CreateScope() and dispose it for every attempt, preferably with a
using declaration or statement. Resolve ApplicationDbContext from that disposed
scope and preserve the existing transaction and retry behavior.

In `@frontend/src/features/warehouses/pages/WarehousesPage.tsx`:
- Around line 55-63: Remove the empty catch from the mutateAsync call in submit
so creation failures remain rejected and the form action does not resolve
successfully; preserve the existing validation and setError behavior.

---

Outside diff comments:
In
`@backend/src/InventoryFlow.Infrastructure/Purchases/EfPurchaseReceiptService.cs`:
- Around line 14-84: Remove the orphaned persistence block after the return from
strategy.ExecuteAsync in RecordAsync, including its duplicate supplier check,
ledger write, receipt creation, save, rollback, and commit logic. Keep the
complete implementation inside the ExecuteAsync lambda unchanged, using
attemptCtx and its transaction.

In `@frontend/src/features/warehouses/pages/WarehousesPage.tsx`:
- Around line 18-26: Update readError to accept an operation-specific fallback
message, then pass “Unable to load warehouses.” at the warehouse-list failure
call site and “Unable to archive the warehouse.” at the archive failure call
site; preserve the existing save fallback for the save operation.
🪄 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: CHILL

Plan: Pro Plus

Run ID: 76e068a0-50a8-4230-bd8b-8f33f1bdaa3d

📥 Commits

Reviewing files that changed from the base of the PR and between 24edb89 and 4e1c281.

📒 Files selected for processing (37)
  • backend/Dockerfile
  • backend/src/InventoryFlow.Api/ExceptionHandling/GlobalExceptionHandler.cs
  • backend/src/InventoryFlow.Application/Common/Tenancy/CurrentWorkspace.cs
  • backend/src/InventoryFlow.Application/Features/Authentication/AuthenticationValidators.cs
  • backend/src/InventoryFlow.Application/Features/Warehouses/WarehouseValidators.cs
  • backend/src/InventoryFlow.Domain/Entities/PurchaseReceipt.cs
  • backend/src/InventoryFlow.Infrastructure/Inventory/EfInventoryLedger.cs
  • backend/src/InventoryFlow.Infrastructure/Inventory/InventoryLedgerWriter.cs
  • backend/src/InventoryFlow.Infrastructure/Persistence/Configurations/InventoryBalanceConfiguration.cs
  • backend/src/InventoryFlow.Infrastructure/Persistence/Configurations/InventoryMovementConfiguration.cs
  • backend/src/InventoryFlow.Infrastructure/Persistence/Configurations/ProductConfiguration.cs
  • backend/src/InventoryFlow.Infrastructure/Persistence/Configurations/PurchaseReceiptConfiguration.cs
  • backend/src/InventoryFlow.Infrastructure/Persistence/Configurations/RefreshTokenConfiguration.cs
  • backend/src/InventoryFlow.Infrastructure/Persistence/Configurations/SalesFulfillmentConfiguration.cs
  • backend/src/InventoryFlow.Infrastructure/Persistence/Configurations/SupplierConfiguration.cs
  • backend/src/InventoryFlow.Infrastructure/Persistence/Configurations/WarehouseConfiguration.cs
  • backend/src/InventoryFlow.Infrastructure/Persistence/Configurations/WarehouseTransferConfiguration.cs
  • backend/src/InventoryFlow.Infrastructure/Persistence/Configurations/WorkspaceConfiguration.cs
  • backend/src/InventoryFlow.Infrastructure/Persistence/Configurations/WorkspaceInvitationConfiguration.cs
  • backend/src/InventoryFlow.Infrastructure/Products/EfProductCatalog.cs
  • backend/src/InventoryFlow.Infrastructure/Purchases/EfPurchaseReceiptService.cs
  • backend/src/InventoryFlow.Infrastructure/Sales/EfSalesFulfillmentService.cs
  • backend/src/InventoryFlow.Infrastructure/Transfers/EfWarehouseTransferService.cs
  • backend/tests/InventoryFlow.IntegrationTests/Api/AuthenticatedApiFixture.cs
  • backend/tests/InventoryFlow.IntegrationTests/Api/InventoryEndpointsTests.cs
  • backend/tests/InventoryFlow.IntegrationTests/Api/InventoryFlowApiFactory.cs
  • docker-compose.yml
  • frontend/src/components/layout/Topbar.tsx
  • frontend/src/features/dashboard/components/MetricCard.tsx
  • frontend/src/features/dashboard/pages/DashboardPage.tsx
  • frontend/src/features/products/pages/ProductsPage.tsx
  • frontend/src/features/purchases/pages/PurchasesPage.tsx
  • frontend/src/features/sales/pages/SalesPage.tsx
  • frontend/src/features/suppliers/pages/SuppliersPage.tsx
  • frontend/src/features/warehouses/pages/WarehousesPage.tsx
  • frontend/src/lib/api-client.ts
  • global.json
🚧 Files skipped from review as they are similar to previous changes (33)
  • backend/src/InventoryFlow.Infrastructure/Persistence/Configurations/WorkspaceConfiguration.cs
  • backend/src/InventoryFlow.Infrastructure/Persistence/Configurations/SupplierConfiguration.cs
  • backend/src/InventoryFlow.Application/Features/Warehouses/WarehouseValidators.cs
  • backend/src/InventoryFlow.Infrastructure/Persistence/Configurations/InventoryBalanceConfiguration.cs
  • backend/src/InventoryFlow.Infrastructure/Inventory/InventoryLedgerWriter.cs
  • frontend/src/features/products/pages/ProductsPage.tsx
  • backend/src/InventoryFlow.Infrastructure/Persistence/Configurations/RefreshTokenConfiguration.cs
  • backend/src/InventoryFlow.Infrastructure/Persistence/Configurations/ProductConfiguration.cs
  • backend/src/InventoryFlow.Infrastructure/Persistence/Configurations/WorkspaceInvitationConfiguration.cs
  • backend/src/InventoryFlow.Application/Common/Tenancy/CurrentWorkspace.cs
  • backend/src/InventoryFlow.Infrastructure/Persistence/Configurations/SalesFulfillmentConfiguration.cs
  • backend/src/InventoryFlow.Infrastructure/Persistence/Configurations/InventoryMovementConfiguration.cs
  • backend/src/InventoryFlow.Infrastructure/Persistence/Configurations/WarehouseTransferConfiguration.cs
  • backend/src/InventoryFlow.Infrastructure/Inventory/EfInventoryLedger.cs
  • backend/src/InventoryFlow.Infrastructure/Persistence/Configurations/PurchaseReceiptConfiguration.cs
  • frontend/src/features/dashboard/components/MetricCard.tsx
  • backend/src/InventoryFlow.Infrastructure/Persistence/Configurations/WarehouseConfiguration.cs
  • backend/tests/InventoryFlow.IntegrationTests/Api/AuthenticatedApiFixture.cs
  • backend/src/InventoryFlow.Domain/Entities/PurchaseReceipt.cs
  • frontend/src/features/sales/pages/SalesPage.tsx
  • backend/tests/InventoryFlow.IntegrationTests/Api/InventoryFlowApiFactory.cs
  • frontend/src/components/layout/Topbar.tsx
  • docker-compose.yml
  • backend/src/InventoryFlow.Infrastructure/Sales/EfSalesFulfillmentService.cs
  • frontend/src/features/dashboard/pages/DashboardPage.tsx
  • frontend/src/features/purchases/pages/PurchasesPage.tsx
  • backend/Dockerfile
  • backend/src/InventoryFlow.Infrastructure/Transfers/EfWarehouseTransferService.cs
  • backend/src/InventoryFlow.Api/ExceptionHandling/GlobalExceptionHandler.cs
  • backend/src/InventoryFlow.Infrastructure/Products/EfProductCatalog.cs
  • frontend/src/features/suppliers/pages/SuppliersPage.tsx
  • backend/tests/InventoryFlow.IntegrationTests/Api/InventoryEndpointsTests.cs
  • frontend/src/lib/api-client.ts

Comment thread backend/src/InventoryFlow.Infrastructure/Purchases/EfPurchaseReceiptService.cs Outdated
Comment thread frontend/src/features/warehouses/pages/WarehousesPage.tsx

@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 (1)
frontend/src/features/warehouses/pages/WarehousesPage.tsx (1)

55-63: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Swallow the rejected mutation promise here.
create.mutateAsync(parsed.data) can reject and bubble out of the form action callback. The other create flows already use catch(() => {}); do the same here so a failed save still shows the onError message without surfacing an unhandled rejection.

🤖 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 `@frontend/src/features/warehouses/pages/WarehousesPage.tsx` around lines 55 -
63, Update the submit function to handle rejection from
create.mutateAsync(parsed.data) by swallowing the promise error, matching the
catch behavior used by the other create flows; preserve the existing onError
message handling and successful submission behavior.

Source: Path instructions

🤖 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
`@backend/src/InventoryFlow.Infrastructure/Purchases/EfPurchaseReceiptService.cs`:
- Around line 20-24: Update the scope creation in the strategy.ExecuteAsync
callback to use the asynchronous scope factory method, CreateAsyncScope(), so
the existing await using declaration in the purchase receipt flow compiles and
disposes the scope correctly on each retry.

In `@frontend/src/features/warehouses/pages/WarehousesPage.tsx`:
- Around line 49-53: Update the WarehousesPage archive mutation and UI to use a
dedicated archiveError state instead of the add-warehouse error state. Have
archive.onError set archiveError, clear archiveError on successful archiving,
and render the archive error near the catalog controls while leaving the
add-warehouse form error handling unchanged.

---

Outside diff comments:
In `@frontend/src/features/warehouses/pages/WarehousesPage.tsx`:
- Around line 55-63: Update the submit function to handle rejection from
create.mutateAsync(parsed.data) by swallowing the promise error, matching the
catch behavior used by the other create flows; preserve the existing onError
message handling and successful submission behavior.
🪄 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: CHILL

Plan: Pro Plus

Run ID: 13e3e602-3491-4dca-a46a-a9a9daae12ce

📥 Commits

Reviewing files that changed from the base of the PR and between 4e1c281 and c9eab4e.

📒 Files selected for processing (2)
  • backend/src/InventoryFlow.Infrastructure/Purchases/EfPurchaseReceiptService.cs
  • frontend/src/features/warehouses/pages/WarehousesPage.tsx

Comment thread frontend/src/features/warehouses/pages/WarehousesPage.tsx
@imadnan4
imadnan4 merged commit b50f6c5 into main Jul 25, 2026
1 of 3 checks passed
@imadnan4
imadnan4 deleted the review/full-codebase branch July 25, 2026 15:17
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.

2 participants