5.2 KiB
Session Example: Quote-to-RO Service Sync
Project Context
- SPQ-v2 (ShopProQuote): React/Vite/TS shop management app
- Backend: PocketBase v0.39.5 (self-hosted Docker container at 127.0.0.1:8091)
- State: Zustand with
persistmiddleware (localStorage key:spq-quote) - API rules: All user-scoped collections use
userId = @request.auth.id - App user login: pocketbase collections
users(not superuser)
The Bug
When customer-approved recommended services from a Quote (generated from an RO) were saved, they did NOT transfer back to the originating Repair Order.
Root Cause 1 (Structural): Transient State Not Restored on Edit
The repairOriginId state variable was initialized from a URL query param (?fromRO=RO_ID) but never restored from the persisted repairOrderId field when re-opening the quote for editing (?edit=QUOTE_ID):
// QuoteGenerator.tsx — the fromRO effect only ran on mount with [] deps
// When editing, fromRO wasn't in the URL, so repairOriginId stayed null
Fix applied by previous agent: Added setRepairOriginId(data.repairOrderId) in the edit-loading useEffect (QuoteGenerator.tsx:523-525).
Root Cause 2 (Auth/Ownership): API Rules Blocked Cross-User Writes
Even after the structural fix, the sync STILL failed. Investigation revealed:
-
The app user
demo@shop.comdidn't exist in PocketBase. The actual data belonged tomani8994@gmail.com. Logging in asdemo@shop.comshowed the correct data due to Zustand persist rehydrating from localStorage — but any API call (load, save) was blocked. -
PocketBase's
userId = @request.auth.idrule on therepairOrderscollection blocked the RO update when the user didn't own the RO. The sync code'spb.collection('repairOrders').update()returned a 404 (record "not found" from the auth perspective), which was caught by the innercatchand logged as a toast.
The Full Investigation (Multi-Layer)
Layer 1 — Code inspection
- Confirmed sync code exists in
QuoteSummary.tsx:handleSave(lines 145-194) andensureShareToken(lines 267-308) - Confirmed the edit-loading code restores
repairOriginIdfromdata.repairOrderId - Confirmed
sanitizeServicesdoesn't stripapproved(only null values) - Confirmed Zustand persist rehydration is synchronous and the edit-load effect overwrites PB data correctly
Layer 2 — Database inspection (Docker + curLl)
- Authenticated as superuser via PocketBase CLI:
docker exec pocketbase /usr/local/bin/pocketbase superuser upsert admin@shop.com TEMP_PASS --dir=/pb_data - Queried quotes and ROs via the reST API:
POST /api/collections/_superusers/auth-with-password GET /api/collections/quotes/records?perPage=100&fields=id,repairOrderId,customerName,services - Found 10+ quotes with
repairOrderIdpopulated AND approved services - Found linked ROs had 0 services — confirming the sync never fired successfully
- Used
filter=repairOrderId!=%27%27to find only linked quotes efficiently
Layer 3 — Direct API test (curl as app user)
- Authenticated as
mani8994@gmail.com(the record owner) and performed the exact samePATCH /api/collections/repairOrders/records/RO_IDwith the same payload the frontend would send:curl -s -X PATCH "http://127.0.0.1:8091/api/collections/repairOrders/records/RO_ID" \ -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' \ -d '{"services": "[{\"name\":\"Test\",\"total\":100,\"status\":\"pending\"}]"}' - The API call SUCCEEDED — the RO now had services. This proved the code logic is correct.
- Created fresh test data (RO + quote) owned by the same user to reproduce from scratch
Layer 4 — Auth verification
- Confirmed
demo@shop.comcould NOT update the RO owned bymani8994@gmail.com(returned 404) - Confirmed
mani8994@gmail.comCOULD update the RO (returned 200) - Conclusion: The sync code is correct BUT the frontend must be authenticated as the record owner
Layer 5 — ensureShareToken sync gap
- When
editIdis set (editing an existing quote), theensureShareTokenfunction skips the entireif (!quoteId)block — including the sync code - This means the sync only runs for NEW quote shares, not for shares of existing quotes
- The
handleSavepath's sync runs for both new and edit saves, so Save+Share in sequence works, but Share-without-intervening-Save doesn't sync
Key Takeaways
-
Pattern 1 fix is necessary but not sufficient. Adding
setRepairOriginId()to the edit-loading effect is required, but the sync still won't fire if the user doesn't own the target record. -
Zustand persist masks auth issues. LocalStorage rehydration shows correct-looking data even when the authenticated user has no backend access. Saves silently fail.
-
Verify at the API level. When the UI shows correct state but the backend doesn't have it, test the API call directly. curl never lies.
-
Create test data with known ownership. Test data created via the frontend (authenticated as the target user) avoids cross-owner issues.
-
Multi-path sync code must be audited for gaps. When the same sync logic is copy-pasted in
handleSaveandensureShareToken, the two code paths can diverge.ensureShareTokenonly syncs on new quote creation.