Files
hermes-config/skills/software-development/react-crud-patterns/references/quote-ro-sync-example.md
T
2026-07-12 10:17:17 -04:00

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 persist middleware (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:

  1. The app user demo@shop.com didn't exist in PocketBase. The actual data belonged to mani8994@gmail.com. Logging in as demo@shop.com showed the correct data due to Zustand persist rehydrating from localStorage — but any API call (load, save) was blocked.

  2. PocketBase's userId = @request.auth.id rule on the repairOrders collection blocked the RO update when the user didn't own the RO. The sync code's pb.collection('repairOrders').update() returned a 404 (record "not found" from the auth perspective), which was caught by the inner catch and logged as a toast.

The Full Investigation (Multi-Layer)

Layer 1 — Code inspection

  • Confirmed sync code exists in QuoteSummary.tsx:handleSave (lines 145-194) and ensureShareToken (lines 267-308)
  • Confirmed the edit-loading code restores repairOriginId from data.repairOrderId
  • Confirmed sanitizeServices doesn't strip approved (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 repairOrderId populated AND approved services
  • Found linked ROs had 0 services — confirming the sync never fired successfully
  • Used filter=repairOrderId!=%27%27 to 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 same PATCH /api/collections/repairOrders/records/RO_ID with 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.com could NOT update the RO owned by mani8994@gmail.com (returned 404)
  • Confirmed mani8994@gmail.com COULD 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 editId is set (editing an existing quote), the ensureShareToken function skips the entire if (!quoteId) block — including the sync code
  • This means the sync only runs for NEW quote shares, not for shares of existing quotes
  • The handleSave path'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

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

  2. Zustand persist masks auth issues. LocalStorage rehydration shows correct-looking data even when the authenticated user has no backend access. Saves silently fail.

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

  4. Create test data with known ownership. Test data created via the frontend (authenticated as the target user) avoids cross-owner issues.

  5. Multi-path sync code must be audited for gaps. When the same sync logic is copy-pasted in handleSave and ensureShareToken, the two code paths can diverge. ensureShareToken only syncs on new quote creation.