Configured Commerce Upgrade Watch: December 2025
sts 5.2.2512.357+sts Covers December 2025
Written by Lance Farquhar January 4, 2026
Release 5.2.2512.357+sts, published December 22, 2025. Each month we read what Optimizely says shipped, then go verify it against the actual source — frontend and backend — so you get more than a changelog recap.
The 30-Second Version
- Headline feature: Order Approval Editing — approvers and submitters can now fix up an order that's pending approval instead of scrapping it and starting over.
- Worth knowing: Stripe Radar fraud-detection support landed in the Payment Service, and the ODP integration got a real new field for syncing order status.
- One low-risk breaking change: a new method on a core settings interface — a two-minute check if you've extended it.
- Our verdict: solid, well-built release. The approval workflow is a genuine state-machine addition, not a UI facelift, and the Stripe Radar wiring is a clean piece of full-stack plumbing worth learning from.
The Headline: Order Approval Editing
Before this release, if an approver had a problem with an order pending their sign-off, the options were "approve it" or "reject it and make the submitter start over." December's release adds a real middle path: the approver can edit the order directly, or hand it back to the submitter with a note, without either side losing their place.
What makes this worth a deep dive rather than a bullet point is that it's implemented as an actual state machine, not a couple of new buttons bolted onto the approval screen. The order-approval cart picked up two new statuses:
Both new states are driven by dedicated handler chains. Here's the one that lets an approver take an order into edit mode:
// Store/Pages/OrderApprovalDetails/Handlers/EditOrder.ts
export const PopulateApiParameter: HandlerType = props => {
props.apiParameter = {
cart: {
...props.parameter.cart,
orderApprovalStatus: "EditingInProcess",
},
};
};
export const SendDataToApi: HandlerType = async props => {
await updateCart(props.apiParameter);
};
export const chain = [PopulateApiParameter, SendDataToApi, ResetData, ExecuteOnSuccessCallback];
And the "hand it back" path, which also carries a message back to whoever submitted the order:
// Store/Pages/OrderApprovalDetails/Handlers/RequestRevision.ts
type HandlerType = ApiHandlerDiscreteParameter<
{ cart: CartModel; requestRevisionMessage?: string } & HasOnSuccess,
UpdateCartApiParameter,
CartModel
>;
export const PopulateApiParameter: HandlerType = props => {
props.apiParameter = {
cart: {
...props.parameter.cart,
orderApprovalStatus: "RevisionRequested",
requestRevisionMessage: props.parameter.requestRevisionMessage,
},
};
};
Three more handlers (CancelApproval, DiscardEdit, SaveOrderApproval) round out the rest of the state machine, and a brand-new transactional email goes out the moment a revision is requested, so the submitter doesn't have to go looking for it:
<!-- _SystemResources/Views/DefaultEmails/RequestRevision.cshtml -->
<h2>Approver requested a revision for the [[Model.OrderNumber]] order approval</h2>
<p>[[Model.Message]]</p>
<a class="view-order" href="[[Model.OrderUrl]]">View Order</a>
Five new handlers, two new cart statuses, and a new notification template — that's a real feature, and it's built the way you'd want it built.
What Else Shipped
Breaking change (low risk): Guid? GetCurrentWebsiteId() was added to ISystemSettingProvider, the interface behind Configured Commerce's settings system, fixing a case where settings weren't resolving correctly on .NET 8+. If you've written your own implementation of that interface — which is uncommon but not rare — add the method. Everyone else is unaffected.
Stripe Radar support was added to the Payment Service — more on exactly how it's wired below, because it's a nice piece of engineering.
ODP integration got a new ODP Action Type field on the Order Status Mapping table, so Purchase and Cancel order events can be pushed to Optimizely Data Platform. Also covered below, with the actual migration.
Everything else was routine and checks out cleanly against source:
- Product Import no longer demands a Picture ID/URL on every row when you're not touching images.
- A GUID-parsing bug in Retail Search facet processing is fixed (detail below).
- Newly created addresses now show up in the Assign Ship-To modal without a page refresh.
- A handful of items — a TokenEx race condition, Spire logging, a couple of autocomplete edge cases, a guest-customer restriction setting, and a field-mapper batch-size bump — were hotfixed onto November's release and are just being documented here now.
What We Actually Found: Stripe Radar, Done Right
Every post in this series, we go looking for something worth calling out beyond "the notes match the code." This month it's not a discrepancy — it's an example of an integration that's genuinely well built, and small enough to learn from in five minutes.
Stripe Radar is Stripe's fraud-scoring product: it fingerprints the browser session and hands back a risk signal alongside the charge. Wiring it in touches three different layers — a third-party script, your frontend, and your backend — and it's easy to make that messy. Configured Commerce didn't:
The frontend half is a single hook that loads Stripe's script, opens a session, and hands the resulting ID back to whoever's listening — nothing else on the page needs to know Radar exists:
// Common/Hooks/useStripeSession.ts
export const useStripeSession = (onSessionUpdate: (sessionId: string) => void) => {
useEffect(() => {
if (!websiteSettings.enabledSpreedlyStripeRadar || !websiteSettings.stripePublishableKey) {
return;
}
const radarScript = document.createElement("script");
radarScript.src = "https://js.stripe.com/v3";
radarScript.onload = () => {
const stripe = Stripe(websiteSettings.stripePublishableKey);
stripe.createRadarSession().then(response => {
const radarSessionId = response.radarSession?.id ?? null;
if (radarSessionId) {
onSessionUpdate(radarSessionId);
}
});
};
document.body.appendChild(radarScript);
return () => document.body.removeChild(radarScript);
}, []);
};
On the backend, the entire integration is one added field on the checkout request that gets threaded straight into the gateway call:
// Payments/Services/PaymentService.cs
MaskedCardNumber = userPaymentProfile.MaskedCardNumber,
BrowserInfo = browserInfo,
SecurityCode = securityCode,
AntiFraudSessionId = parameter.CreditCard?.AntiFraudSessionId ?? string.Empty,
No new endpoint, no schema change, no extra round trip — the session ID rides along with data that was already flowing through checkout. That's the mark of an integration designed by someone who understood the existing checkout pipeline well enough to not need to bend it.
Under the Hood
Two more changes worth a closer look, because both are small enough to read in full and both are exact, word-for-word matches for what the release notes describe — which isn't a given (more on that methodology in a future post).
The ODP field is a single, dated migration:
-- DatabaseScripts/5.2/2025.12.05.01.AddOdpActionTypeToOrderStatusMapping.sql
ALTER TABLE [dbo].[OrderStatusMapping]
ADD ODPActionType [nvarchar](50) NOT NULL CONSTRAINT [DF_OrderStatusMapping_ODPActionType] DEFAULT ('None');
Table, column, and default all line up exactly with what the notes describe. About as clean a confirmation as this kind of digging ever turns up.
The Retail Search fix is one guard clause, in exactly the right place:
// GoogleCloudRetailSearchProductMapper.cs
if (facet is { Key: not null, Values: not null })
{
if (facet.Key is "brands" or "categories")
{
continue;
}
aggregations[facet.Key.CommerceFacetName()] = new AggregationContainer<T>
{
Items = facet
// ...
};
}
The bug was GUID-parsing errors from "dynamic system field conflicts" — which sounds vague until you know that brands and categories are dynamic system facets, not GUIDs, and this code was feeding them into a GUID-keyed lookup. The fix is two lines, and it takes real familiarity with how Configured Commerce's facet system is put together to know exactly which two keys to skip. Small fix, sharp diagnosis.
What This Means for You
- If you've implemented
ISystemSettingProvideryourself, addGetCurrentWebsiteId()— it's a five-minute check, and it's the only thing in this release that can break custom code. - If you want Stripe Radar, it's opt-in behind a website setting (
enabledSpreedlyStripeRadar) plus your Stripe publishable key — nothing else to build. - If you're on the ODP integration, you now have a real Purchase/Cancel action field to map in the Order Status Mapping table instead of working around its absence.
- Everything else — the import fix, the search fix, the Ship-To modal fix — is safe to take as-is; we checked, and it does what it says.
Upgrading a Configured Commerce storefront and want a second set of eyes on what actually changed under a release before you take it? That's exactly the kind of review Nishtech does for clients every month — reach out and we'll walk through it with you.