Skip to content

feat: enhance offline batch and serial number management - #359

Open
MohamedAliSmk wants to merge 22 commits into
developfrom
PN-83-Batch-Serial-unable-to-select-at-offline-mode
Open

MohamedAliSmk wants to merge 22 commits into
developfrom
PN-83-Batch-Serial-unable-to-select-at-offline-mode

Conversation

@MohamedAliSmk

@MohamedAliSmk MohamedAliSmk commented Sep 3, 2026 •

Copy link
Copy Markdown
Collaborator
  • Added functions to persist and consume batch and serial data for items in offline mode.
  • Implemented caching for batch and serial numbers during item searches.
  • Improved handling of cached data to ensure accurate updates and retrievals when offline.
  • Enhanced error handling for background caching operations.

🛑 Review action list — do not merge/close until all checked

Full review · 11 inline comments · details in each

Blocking

  • T1 Batch qty subtracted twice offline — delete consumeCachedBatchQty
  • T2 Batch consume has no return path (4 drift paths) — fixed by T1
  • T3 Sold serials returned to cache on offline submit — flag in clearCart

Should fix

  • T4 Cache mutators non-atomic — wrap in db.transaction("rw", …)
  • T5 Offline serial fallback skips isCacheValid (warehouse + TTL)
  • T6 Up to 25 sequential POSTs per search — drop pagination sites, dedupe

Nice to have

  • T7 Phantom db.items rows + undefined warehouse guard
  • T8 Vitest config + one items.test.js (repo has zero tests)
  • T9 Wrap 118-char import

- Added functions to persist and consume batch and serial data for items in offline mode.
- Implemented caching for batch and serial numbers during item searches.
- Improved handling of cached data to ensure accurate updates and retrievals when offline.
- Enhanced error handling for background caching operations.

@engahmed1190 engahmed1190 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔍 Full review — code logic, security, performance, coverage

Verdict: request changes. The offline read fix is correct and should ship. The consumption-accounting layer bolted on top has three high-severity defects and wasn't part of PN-83.

Structural read

What actually fixes "Batch/Serial unable to select at offline mode" is ~30 lines:

Change Why it fixes PN-83
persistItemBatchSerialData in batchesResource.onSuccess the dialog's own batch fetch was never written to IndexedDB
persistItemBatchSerialData in fetchSerials serial store results were never written to IndexedDB
the two new return guards in loadBatchesOrSerials offline previously fell through to batchesResource.reload(), which fails

Everything else — the three new cacheBatchSerialForItems call sites, consumeCachedBatchQty, consumeCachedSerials, returnCachedSerials, and the create-on-missing db.items.put — is offline inventory accounting. Every defect below lives in that second layer. Cutting it removes ~90 of the 161 added lines and every high-severity finding.

Security

No new attack surface. No new endpoints, no new server-side code, no HTML/URL/query sink. get_batch_serial_data_for_items is @frappe.whitelist() and unchanged by this diff. All new code writes client-side IndexedDB, already trusted-per-device here.

The security-relevant findings are the inventory-integrity ones: T1/T2 understate stock, T3 lets the same serial reach two invoices. For a POS that's the risk surface that matters.

Coverage

Zero. find . -name "*.test.*" returns nothing, and there's no test block in POS/vite.config.js despite test / test:run / test:coverage in package.json — yarn test cannot run today. This diff adds four pure-ish mutators with real arithmetic (set diffing, Math.max(0, qty - n), sort-merge); they're the most testable and highest-consequence code in the changeset.

Verified

  • Patch applies cleanly to current develop (git apply --3way) — no conflicts, no duplicated call sites.
  • npx biome lint on the changed files: no new violations (one pre-existing useOptionalChain at BatchSerialDialog.vue:365).
  • T1 reproduced with a standalone script — see the inline comment.

✅ Task list — please do not merge or close this PR until every box is checked

🔴 Blocking

  • T1 — Batch qty subtracted twice offline. Delete consumeCachedBatchQty; availableBatches already handles cart subtraction. BatchSerialDialog.vue:516
  • T2 — Batch consumption has no return path (4 divergence paths: line remove, qty edit, addItem uom-merge dropping batch_no, addItem throw). Resolved by T1. items.js:254
  • T3 — Sold serials returned to durable cache on offline submit. Fix in clearCart (useInvoice.js:1182) with a returnSerials flag, false from the two post-submit call sites. serialNumber.js:182

🟡 Should fix

  • T4 — Wrap the surviving cache mutators in db.transaction("rw", db.items, ...); they're non-atomic read-modify-write with .catch(() => {}) at every call site. items.js:210
  • T5 — Offline serial fallback bypasses isCacheValid (warehouse + 5-min TTL). BatchSerialDialog.vue:460
  • T6 — Drop the two pagination cacheBatchSerialForItems sites; keep the search one with a recently-fetched Set + TTL. Up to 25 sequential POSTs per search today. itemSearch.js:1224, 1339, 1725

🟢 Nice to have

  • T7 — Don't put phantom item rows (no item_name/barcodes); guard the undefined warehouse in returnCachedSerials. items.js:173, 236
  • T8 — Add a test block to POS/vite.config.js + one items.test.js with db mocked, covering the consume/return round-trip, Math.max(0, …) clamping, and parseSerialNumbers on both string and array input. Not a suite — one file.
  • T9 — Wrap the 118-char import at BatchSerialDialog.vue:304.

Latent / no action needed this PR

  • Persisted batch/serial data has no warehouse key. Harmless today (dialog is bound to shiftStore.profileWarehouse, POSSale.vue:613) but a shift/profile change leaves stale data with nothing to invalidate it. Worth an issue.

🤖 Generated with Claude Code

https://claude.ai/code/session_016WM1HqLHEhF8GhYjB4EJHp

Comment thread POS/src/components/sale/BatchSerialDialog.vue
Comment thread POS/src/components/sale/BatchSerialDialog.vue Outdated
Comment thread POS/src/components/sale/BatchSerialDialog.vue Outdated
Comment thread POS/src/components/sale/BatchSerialDialog.vue Outdated
Comment thread POS/src/utils/offline/items.js Outdated
Comment thread POS/src/utils/offline/items.js Outdated
Comment thread POS/src/utils/offline/items.js Outdated
Comment thread POS/src/stores/serialNumber.js Outdated
Comment thread POS/src/stores/itemSearch.js
Comment thread POS/src/stores/itemSearch.js Outdated
@engahmed1190 engahmed1190 mentioned this pull request Sep 9, 2026
4 tasks
MohamedAliSmk and others added 9 commits September 10, 2026 11:28
availableBatches already subtracts cart qty from warehouseBatches;
consumeCachedBatchQty was decrementing IndexedDB as well, under-reporting stock offline.

Co-authored-by: Cursor <cursoragent@cursor.com>
Batch consume had no symmetric return, so line-remove, qty-edit, uom-merge,
and addItem throw permanently drifted offline stock. Deleting the mutator
closes all four paths; availableBatches remains the source of truth.

Co-authored-by: Cursor <cursoragent@cursor.com>
clearCart could not tell abandoned cart from sold cart, so offline submit
wrote sold serials back into IndexedDB. Pass returnSerials:false from both
payment-success paths; abandoned clears still restore by default.

Co-authored-by: Cursor <cursoragent@cursor.com>
Wrap persist/update/consume/return in db.transaction('rw', db.items) so
concurrent consumes or races with background cache writes cannot silently
drop updates.

Co-authored-by: Cursor <cursoragent@cursor.com>
Raw serialStore.getSerials() ignored warehouse match and 5-minute TTL.
IndexedDB via getCachedSerialData already covers offline after fetchSerials persists.

Co-authored-by: Cursor <cursoragent@cursor.com>
Pagination sites duplicated shift-start sync and could fire 20-per-request
sequential POSTs per page. Keep search-path caching with a 5-minute
recently-fetched Set so the same items are not re-fetched every search.

Co-authored-by: Cursor <cursoragent@cursor.com>
Do not put incomplete {item_code, batch/serial} rows that blank the product
grid. returnCachedSerials now takes an explicit warehouse (or refuses when
the cache is empty so warehouse would be undefined).

Co-authored-by: Cursor <cursoragent@cursor.com>
Enable yarn test via a vite test block and cover parseSerialNumbers plus
consume/return round-trip, phantom-row skip, and warehouse guard. Batch
qty Math.max clamping left with consumeCachedBatchQty (T1/T2).

Co-authored-by: Cursor <cursoragent@cursor.com>
Match the multi-line import style used elsewhere in the file.

Co-authored-by: Cursor <cursoragent@cursor.com>
@MohamedAliSmk

Copy link
Copy Markdown
Collaborator Author

Review follow-up (local, not pushed yet)

Addressed T1–T9 as 9 separate commits on PN-83-Batch-Serial-unable-to-select-at-offline-mode. Inline replies are on each thread.

Commit Point
4b5eb310 T1 — remove consumeCachedBatchQty call
59e6f51a T2 — delete consumeCachedBatchQty
65fba4f3 T3 — clearCart({ returnSerials: false }) post-submit
b2adb1cf T4 — Dexie transactions on surviving mutators
ddf82e24 T5 — drop invalid in-memory serial fallback
bd6f34d4 T6 — drop pagination sites; TTL-dedupe search
3d879240 T7 — no phantom rows; warehouse guard on return
93d58875 T8 — vite test block + items.test.js (5 tests green)
438f9ddb T9 — wrap offline items import

T8 note: Math.max(0, …) clamping lived only in consumeCachedBatchQty, which left with T1/T2 — tests cover consume/return round-trip, parseSerialNumbers, phantom skip, and warehouse guard instead.

Holding push until author review of the local commits.

@MohamedAliSmk

Copy link
Copy Markdown
Collaborator Author

T8 — tests

Agreed the mutators were the most testable / highest-consequence part of the changeset, and yarn test couldn’t run without a Vite test block.

Done in 93d58875:

  • added a test block to POS/vite.config.js (jsdom, src/**/*.{test,spec}.{js,ts})
  • added POS/src/utils/offline/items.test.js with db mocked

Coverage included:

  • parseSerialNumbers on string + array (+ empty)
  • consume → return round-trip
  • phantom-row skip when item is missing
  • warehouse guard when cache is empty

Note: Math.max(0, …) clamping lived only inside consumeCachedBatchQty, which was removed in T1/T2, so that case isn’t tested anymore.

yarn test:run src/utils/offline/items.test.js → 5 passed.

engahmed1190 and others added 12 commits September 24, 2026 01:57
Brings yarn lint back to the base count (212).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Move items.test.js under utils/offline/__tests__/ to match repo convention
- Drop the vite test block; develop runs its suite without one
- Mark batch/serial codes as fetched before the request so concurrent
  searches skip in-flight codes; unmark on failure so a retry happens

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saving a draft returned its serials to the offline cache, and loading the
draft never consumed them again. Offline, a parked serial could be sold in
another sale and then submitted a second time from the draft.

- Save-draft clears the cart with returnSerials: false
- clearCart defaults to keeping serials when the cart came from a draft
- User deletes (single, clear-all, invoice management) return the draft's
  serials, except for the draft loaded in the cart
- Post-submit draft deletes keep them consumed (default)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
consumeSerials/returnSerials returned early when the in-memory serial cache
had no entry for the item. Offline that cache is never filled (only an online
fetchSerials fills it), so the IndexedDB updates never ran: a serial in the
cart or parked in a draft stayed selectable and could be sold twice.

- Write to the durable cache before the in-memory early return
- Set the dialog warehouse before the offline branch so returned serials
  keep their warehouse when the cache is empty
- serialNumber.spec.js pins both paths (fails on the previous code)

Verified end to end in the browser offline: cart, hold draft, delete draft,
offline sale, sync (serial Delivered on the server).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
setDefaultCustomer only asked the server. Offline the request failed and the
error was swallowed, so after a sale, hold or clear the cart had no customer
and stayed empty even back online. Fall back to the customer on the cached
POS Profile (shiftState, persisted for offline use).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Offline sales never touched local stock: once the cart cleared, the item
badge and the batch dialog went back to the last server figures until the
invoice synced, so the POS kept offering stock it had already sold.

On offline commit, deduct the sold qty (qty x conversion_factor) from the
stock store and IndexedDB for tracked items, and from the sold batch in the
cached batch_no_data. The server refresh after sync replaces these values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Syncing queued offline invoices refreshed nothing, so local figures (including
the offline deductions) stayed until an unrelated refresh. syncOfflineInvoices
now emits offlineInvoicesSynced when any invoice synced; POSSale then refreshes
stock from the server, clears the in-memory serial cache and re-fetches
batch/serial data past the search dedupe, replacing every local figure with
server values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ial data

The offline batch dialog came up empty because concurrent writers replaced
whole item rows after batch/serial data had been cached:
- cacheItems (worker and main thread) bulkPut server rows, which carry no
  batch_no_data/serial_no_data
- updateStockQuantities did get() ... put() of the whole row outside a
  transaction, so a stale copy overwrote data written in between

Carry the cached batch/serial fields over inside the same transaction when
re-caching items, and update only the stock fields in updateStockQuantities.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… startup

- The header cache icon read cache stats before the first background item
  caching finished and never re-read them, so a fresh POS showed a red
  "empty" cache while IndexedDB was full. Re-read the stats when the
  background caching completes.
- Invoices queued offline in an earlier session only synced on the next
  offline->online transition. Sync them at startup when online.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
loadAllCustomers asked the server only for customers modified since the
stored last-sync key. When IndexedDB had lost the customers (storage
eviction, cleared site data, failed write) while the key survived, the delta
came back empty and the POS never got customers back: the customer search
spun forever and a customer could not be picked. Ignore the key when the
cache is empty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After an online sale POSSale refreshed stock from the server but left the
cached batch_no_data untouched, so a later drop to offline showed the
pre-sale batch qty and allowed overselling. Deduct the sold batch qty from
the cache on online success as well; the next batch fetch replaces it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Loading /pos with no connection failed: the service worker was served from
/assets/pos_next/pos/ and could not control the /pos page.

- Serve the worker at /pos-sw.js (copied to www/ by the build) and register
  it with scope /pos; drop the old /assets-scoped registration. The page is
  first opened as /pos, so the scope has no trailing slash.
- Reuse the existing network-first pos-page-cache rule for the page, with one
  cache key for every /pos route and no 24h expiry; cache it on first load.
- Precache URLs made absolute (modifyURLPrefix + manifest/icon entries), and
  the Workbox runtime inlined, since the worker no longer sits next to them.
- Remove the /api network-first rule: once the worker controls the page it
  answered the offline ping from cache, so the POS thought it was online.
- Offline start: trust the session cookie when the user check fails for a
  network reason (only an AuthenticationError logs out); keep the cached shift
  when the shift check fails; decide online/offline on the first ping; fall
  back to cached payment methods when the server request fails.

Verified in the browser: cold offline start, batch sale, auto-sync on
reconnect (invoice submitted, stock matches the server).

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

This branch has not been deployed

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