Znode 9 quote edits: invalidate shipping and totals correctly
Tie quote shipping and totals to a revision, return a named calculation snapshot, and reject results computed before the latest edit.
In this guide
- Line change
- Reload inputs
- Invalidate shipment
- Recalculate
- Save matching revision
The removed line that still affects freight
A buyer removes a bulky item from a quote. The line disappears immediately, but shipping still reflects the original shipment. Refreshing the page sometimes fixes it. That inconsistency suggests more than an incorrect number: the application has multiple representations of the quote and no clear rule for invalidating derived state.
Start by listing what changed and what depends on it. Lines influence package composition, weights, restrictions, discounts, shipping methods, tax inputs, and totals. An edit can make a previously selected shipping method invalid even when its old price still looks plausible.
Create a dependency map
Separate authoritative inputs from calculated results. The saved line set, authorized account, destination, and chosen service constraints are inputs. Shipment estimates and totals are derived. A boolean such as 'shipping calculated' is meaningful only for the exact inputs used to produce that calculation.
Trace where the customized Znode 9 flow holds these values: persistence, cart models, quote models, session, and caches. Do not assume removing one session key invalidates every representation. Equally, avoid clearing an entire session when only one quote's calculation is stale. Scope invalidation to the affected quote and its dependent state.
Keep calculation state tied to its inputs
Keep QuoteCalculationState with the owner of one quote. After reloading the authoritative lines or other changed inputs, call InvalidateAfterInputChange. It clears ShippingAmount and TotalAmount and returns the new revision to carry into the calculation.
Pass that revision to TryApply when shipping and totals finish. Read returns a QuoteCalculationSnapshot with named Revision, ShippingAmount, TotalAmount, and IsCalculated properties. The private lock object only synchronizes access to this in-memory instance; it does not carry business data.
Quotes/QuoteCalculationState.cs csharp
using System;
namespace Gcg.DeveloperExamples.Quotes
{
public sealed class QuoteCalculationSnapshot
{
public long Revision { get; }
public decimal? ShippingAmount { get; }
public decimal? TotalAmount { get; }
public bool IsCalculated
{
get
{
return ShippingAmount.HasValue && TotalAmount.HasValue;
}
}
internal QuoteCalculationSnapshot(long revision, decimal? shippingAmount, decimal? totalAmount)
{
Revision = revision;
ShippingAmount = shippingAmount;
TotalAmount = totalAmount;
}
}
// Owned state for one quote; share the instance only through its owner.
public sealed class QuoteCalculationState
{
private readonly object gate = new object();
private long revision;
private decimal? shipping;
private decimal? total;
public long InvalidateAfterInputChange()
{
lock (gate)
{
revision = checked(revision + 1);
shipping = null;
total = null;
return revision;
}
}
public bool TryApply(long calculatedRevision, decimal shippingAmount,
decimal totalAmount)
{
if (shippingAmount < 0 || totalAmount < 0)
throw new ArgumentOutOfRangeException();
lock (gate)
{
if (calculatedRevision != revision)
return false;
shipping = shippingAmount;
total = totalAmount;
return true;
}
}
public QuoteCalculationSnapshot Read()
{
lock (gate)
{
return new QuoteCalculationSnapshot(revision, shipping, total);
}
}
}
}
// Call InvalidateAfterInputChange after reloading authoritative quote lines.
// Carry its revision with the calculation and pass it to TryApply.
// Persisted quotes need a database/API conditional write across processes.
A line removal creates revision 1 and starts a calculation. A destination change creates revision 2 before that calculation finishes. TryApply(1, ...) returns false, so revision 1 cannot restore stale amounts. TryApply(2, ...) accepts the current result, and Read().IsCalculated becomes true. In a web farm, apply the same conditional revision check to the shared persisted quote because an in-process lock cannot coordinate another server.
Do not manufacture a successful estimate
If the shipping provider fails after an edit, the old estimate is no longer a valid fallback unless a documented policy explicitly allows it for unchanged inputs. Return an unresolved calculation state and preserve the buyer's quote. Showing zero freight can accidentally turn an outage into a financial commitment.
Likewise, do not reset all custom fees simply because the carrier amount is stale. Identify which fees depend on lines, destination, account, or the selected service. Recalculate each through its owning rule. Keep each discount and fee formula under its own documented commercial rule.
Test the quote beyond the immediate screen
Use a synthetic quote with two products whose shipment characteristics differ. Remove one line, change quantity, change destination, and switch service. After each action inspect the response, persisted quote, and a fresh session. The displayed method, calculation status, and total should agree with the current inputs.
Introduce a provider timeout, a stale session snapshot, and two overlapping edits. Confirm that an older calculation cannot overwrite a newer quote revision. Check conversion from quote to order as a separate journey: a correct quote screen does not prove that the final order consumes the same validated calculation.
- Verify line additions and removals separately.
- Exercise a method that becomes unavailable after an edit.
- Preserve custom model data during recalculation.
Make invalidation part of the rule
Keep a list of the inputs that invalidate each calculation: lines, quantities, destination, shipping method, and any account-dependent fees. Update that list when a commercial rule changes, and connect each input change to the relevant recalculation path.
The useful question for every amount is simple: which quote revision produced it? Carrying that answer through the request, stored result, and UI makes stale-session and overlapping-request errors easier to diagnose.
References and further reading
Independent guidance from GCG. Znode is a trademark of its owner. Examples use fictional data and are not official platform documentation. Suggest a correction.