fix(admin,api): consistent product cache invalidation, honest stock/price editing#275

merged2
fix/admin-query-misc-bugs
fix/admin-product-stock-price
openedAug 22, 2026byAAviddaram
View on GitHubTwo-way sync enabledPR last imported 20d ago

Stacked on #274.

Problem

Product writes invalidated/patched caches inconsistently. Stock got a hand-rolled optimistic patch (list + instant search) but price/name edits invalidated neither searchProductsInstant nor getProductById — instant-search results and detail pages showed stale prices/stock for up to 30s. The stock save also failed silently: local onError suppressed the global toast while only restoring a text input.

Changes

  • utils/product-cache.ts: single invalidateProductCaches(qc, productId?) — infinite list, instant search, all-products, and detail when known. All product write paths now use it
  • product-card stock save: 40 lines of cache surgery (cancelQueries + two setQueriesData patches + ref rollback) replaced by the variables-while-pending variant; failed saves toast and revert visibly
  • detail page numeric fields: parseNonNegativeNumber/Int helpers with NaN guards and ">= 0" clamps (was parseInt(...) as unknown as string as never)
  • api: setProductStock input requires integer >= 0; updateProductField rejects negative price/stock/discount server-side

Verification

  • typecheck clean across all workspaces
  • manual: edit stock on card → detail page shows new stock immediately
  • manual: edit price on detail → instant search shows new price
  • manual: force a stock-save failure → toast appears, value reverts

Checks

4 successful · 1 skipped
All checks passed
f7adcdc
Synced from GitHub

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (1)
  • v2

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0e1a81f0-b75e-4925-b6de-c6cc5afb2110

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Cchatgpt-codex-connectorBotreviewed · 2 inline threadsAug 22, 2026
Synced from GitHub

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: aabcd9a009

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

apps/admin/src/components/product/product-card.tsxlines 67–69Outdated · aabcd9aOpen thread
……@@ -56,88 +58,35 @@ const ProductCard = ({ product, brands, categories }: ProductCardProps) => {
66+ ...trpc.product.setProductStock.mutationOptions(),
67+ onError: () => {
68+ toast.error("Үлдэгдэл шинэчлэхэд алдаа гарлаа");
69+ },

P2 Badge Restore the stock value after a failed save

When an edited stock mutation fails, stockValue already contains the attempted value from onValueChange, but this handler only displays a toast. The subsequent invalidation usually returns the same unchanged product.stock, so the effect keyed on product.stock does not run and the card continues displaying the unsaved stock indefinitely. Reset stockValue to the cached/server value on error so the inventory shown after a rejected request remains accurate.

Useful? React with 👍 / 👎.

packages/api/src/routers/admin/product.tslines 532–535Outdated · aabcd9aOpen thread
……@@ -524,6 +527,20 @@ export function buildProductRouter<P extends typeof baseProcedure>(proc: P) {
532+ throw new TRPCError({
533+ code: "BAD_REQUEST",
534+ message: `${input.field} must be a non-negative number`,
535+ });
Cchatgpt-codex-connectorBoton lines 532–535Aug 22, 2026

P2 Badge Preserve BAD_REQUEST errors from numeric validation

For a client that submits a negative/non-integral price, stock, or discount, this explicitly created BAD_REQUEST is immediately caught by the mutation's catch block and replaced with INTERNAL_SERVER_ERROR. Consequently invalid user input is reported as a server failure and callers cannot distinguish it from an operational outage; rethrow existing TRPCError instances as the stock mutation already does.

Useful? React with 👍 / 👎.

Synced from GitHub

Greptile Summary

This PR centralizes product-query invalidation, simplifies card stock mutation state, hardens numeric product updates, and adds server-side non-negative validation. Two stock-card paths remain inconsistent with the stated behavior:

  • Failed stock saves retain and display the unsaved local value.
  • Card stock saves omit the product ID needed to invalidate the detail query.

Confidence Score: 3/5

The PR should not merge until failed card stock saves visibly restore server state and successful stock saves invalidate the affected detail cache.

The changed stock flow can display an unsaved value after a request failure and can leave getProductById stale after a successful card-side update.

Files Needing Attention: apps/admin/src/components/product/product-card.tsx

Important Files Changed

FilenameOverview
apps/admin/src/components/product/product-card.tsxReplaces optimistic cache surgery with pending mutation variables, but failed saves retain the edited local value and stock writes omit detail-cache invalidation.
apps/admin/src/utils/product-cache.tsCentralizes invalidation for the primary product query families, with detail invalidation correctly conditional on receiving a product ID.
apps/admin/src/features/products/detail/hooks/use-product-detail-mutations.tsRoutes detail-page product and image mutations through the centralized cache invalidation helper.
apps/admin/src/features/products/detail/product-detail-page.tsxAdds finite non-negative numeric parsers and directly adapts the delete mutation for RowAction.
apps/admin/src/features/products/form/product-form.tsxConsolidates add and update cache invalidation through the new helper.
packages/api/src/routers/admin/product.tsEnforces non-negative stock, price, and discount values and integer stock and discount values at the API boundary.

Flowchart

mermaid
1%%{init: {'theme': 'neutral'}}%%
2flowchart TD
3 A[Admin edits card stock] --> B[setProductStock mutation]
4 B -->|Success| C[Invalidate list, search, all-products]
5 C --> D[Detail cache not invalidated]
6 B -->|Failure| E[Show error toast]
7 E --> F[Local stockValue retained]
8 F --> G[Card displays unsaved stock]

Reviews (1): Last reviewed commit: "fix(admin,api): consistent product cache..." | Re-trigger Greptile

apps/admin/src/components/product/product-card.tsxlines 73–75Outdated · aabcd9aOpen thread
……@@ -56,88 +58,35 @@ const ProductCard = ({ product, brands, categories }: ProductCardProps) => {
72+ },
73+ onSettled: () => {
74+ invalidateProductCaches(queryClient);
75+ },
Ggreptile-appsBoton lines 73–75Aug 22, 2026

P1 Stock write skips detail invalidation

When stock is changed from a product card, this call omits product.id, so invalidateProductCaches does not invalidate the corresponding getProductById entry, causing a cached detail view to continue showing the old stock.

suggestion
1 onSettled: () => {
2 invalidateProductCaches(queryClient, product.id);
3 },
apps/admin/src/components/product/product-card.tsxlines 67–69Outdated · aabcd9aOpen thread
……@@ -56,88 +58,35 @@ const ProductCard = ({ product, brands, categories }: ProductCardProps) => {
66+ ...trpc.product.setProductStock.mutationOptions(),
67+ onError: () => {
68+ toast.error("Үлдэгдэл шинэчлэхэд алдаа гарлаа");
69+ },
Ggreptile-appsBoton lines 67–69Aug 22, 2026

P1 Failed stock value stays visible

When setProductStock fails, onError displays a toast without restoring stockValue; because product.stock remains unchanged, the synchronization effect does not run again, causing the card to continue displaying the unsaved value and potentially a false out-of-stock state.

suggestion
1 onError: () => {
2 setStockValue(product.stock);
3 toast.error("Үлдэгдэл шинэчлэхэд алдаа гарлаа");
4 },

Added 2 commits

  • fix(admin,api): consistent product cache invalidation and honest stock/price editing764a1fd
  • fix(admin,api): review follow-ups for product stock flowf7adcdc

All three addressed in f7adcdc (and 764a1fd): settle invalidation passes product.id so cached detail views refresh; failed saves restore the server value; updateProductField re-throws TRPCError so BAD_REQUEST validation isn't masked as INTERNAL_SERVER_ERROR.