Preserve V2 FlowSpec state during V3 admin sync
This PR makes V3 FlowSpec publishes update only the V3-owned product-selection envelope while preserving V2-owned runtime edits, legacy JSON, and concurrent mutations.
V3 admin sync no longer overwrites the live V2 PricingFlowSpec document with stale converted calculator, filters, or terms state.
V3 owns product selection. V2 remains authoritative for Terms Library, Pricing Calculator, filters, unknown top-level fields, and legacy step variants.
Server-side read-modify-write paths are serialized per organization with a transaction-scoped Postgres advisory lock.
Read-before-write hooks bypass React Query’s stale window so they splice edits into the latest server document.
1. Why this exists
A V3 publish converted the V3 FlowSpec into a full V2 pricingFlowSpecData payload.
- Converted V3 state could contain stale calculator or term data.
- Unknown V2 JSON fields could be stripped by schema parsing.
- Racing V2 and V3 writes could clobber each other.
V3 sync is treated as a fragment update against the latest live V2 document.
- Only the
v3ProductSelectionStepis replaced or inserted. - Existing V2-owned fields and steps are cloned forward verbatim.
- Writes are locked and rebased on the latest row.
Synced from V3 into the V2 runtime row.
Preserved from the latest V2 row during V3 sync.
Read and write happen inside one advisory-locked transaction.
2. What changes
Server: preservation-aware FlowSpec mutations
pricingFlowSpecMutations.tsmergeV3ProductSelectionIntoV2FlowSpec and withPricingFlowSpecMutationLock.
v3Admin/_helpers.tspricingFlowSpec/update.tsTermService.tspricingFlowSpecRepository.tsgetMostRecentRaw and updatePricingFlowSpecDataRaw escape hatches.
Client: fresh read before write
fetchFreshQuery wraps queryClient.fetchQuery with staleTime: 0.
That forces a server read even when the shared React Query cache entry is still fresh.
useFilterMutationsusePricingCalculatorConfiguseProductSelectionConfig
useUpdateFlowSpec now invalidates V2-backed pricing flow and terms queries after V3 publish.
Behavior comparison
| Path | Old behavior | New behavior |
|---|---|---|
| V3 FlowSpec publish | Append a V2 row from full V3 conversion. | Append a V2 row from latest V2 + converted product-selection envelope. |
| Rollback / in-place sync | Overwrite the most recent V2 row with full conversion. | Mutate only product selection in the most recent V2 row. |
| Terms Library save | Parse and rewrite the whole V2 FlowSpec shape. | Lock, read raw latest JSON, mutate terms in category layouts, save raw JSON. |
| Pricing Calculator save | Apply edits to possibly cached V2 data. | Fetch fresh V2 data, apply calculator fragment, then update. |
3. How it works
mergeV3ProductSelectionIntoV2FlowSpec(currentV2, convertedV2) clones currentV2, then replaces or inserts the converted product-selection step.
If the converted document has no product-selection envelope, it throws instead of wiping V2 state.
withPricingFlowSpecMutationLock runs inside prisma.$transaction and executes:
SELECT pg_advisory_xact_lock(hashtextextended(key, 0))
Preservation-aware paths avoid full PricingFlowSpecData parsing because that schema can strip unknown keys or reject legacy steps.
TermService now mutates fragments, not the whole schema
mutatePricingFlowSpec
→
lock organization
→
getMostRecentRaw
updatePricingFlowSpecDataRaw
→
same transaction
→
unknown fields preserved
4. Test coverage added
fetchFreshQuery.test.ts proves the helper runs the query function even when the shared cache entry is still fresh.
Repository tests cover raw latest-row reads and raw JSON updates without schema parsing.
Term tests verify both orderings: Terms-save first and V3-publish first. Both changes survive.
V3 admin tests assert publish and rollback change only product selection while preserving calculator, terms, legacy fields, and unknown config.
git diff --check passing.
5. What it doesn’t change
- No Prisma schema migration and no new database tables or columns.
- No new REST surface; touched API paths remain tRPC-backed.
- No change to V3 ownership of Product Selection.
- No change to the V2 runtime reader contract: runtime/admin still resolve the most recent
PricingFlowSpecrow. - No attempt to make V3 calculator or terms authoritative; stale converted V3 calculator/terms data is intentionally ignored during FlowSpec sync.
- No delete behavior for raw V2 editable collections changes in this diff.