initial commit
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
# 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`):
|
||||
|
||||
```tsx
|
||||
// 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:
|
||||
```bash
|
||||
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:
|
||||
```bash
|
||||
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.
|
||||
Reference in New Issue
Block a user