Developer forum

Forum » Ecommerce - Standard features » QuickPay Payment Window: StateOk discards the API response that could complete the order

QuickPay Payment Window: StateOk discards the API response that could complete the order

Anders Ebdrup
Reply

Hi DynamicWeb,

We're seeing orders fail to complete in the QuickPay Payment Window checkout handler when the callback either doesn't fully process the order or doesn't arrive in time. The customer pays successfully (the money is captured at the bank), but the order stays in Complete:False and the customer is shown the error: "Called State ok, but order is not set complete - this should have happened in the callback."

The interesting part: in every failed case we've inspected, StateOk had already received a QuickPay API response that contained an approved authorize operation. It just didn't use it.

What StateOk does today

When the customer is redirected back with QuickPayState=Ok, StateOk polls until the latest operation is no longer pending:

do
{
    Thread.Sleep(1000);
    status = GetLastOperationStatus(order);
    attempts++;
}
while (status.IsPending && attempts < maxAttempts);

GetLastOperationStatus fetches the full payment from the API, but reduces it to a small OperationStatus object with just IsPending, StatusCode and Succeded – read from operations.Last(). The full response (with all operations) is then discarded.

After the loop, StateOk only checks order.Complete. If the callback hasn't completed the order, it gives up and renders the error template – even though the response it just fetched contained everything needed to complete the order.

Two real cases where this bites us

Case 1 – 3D Secure, Mastercard Debet. API response from StateOk:

"accepted": true,
"operations": [
  { "id": 1, "type": "authorize", "qp_status_code": "30100", "qp_status_msg": "3-D Secure required",
    "created_at": "2026-05-06T09:24:11Z" },
  { "id": 2, "type": "authorize", "qp_status_code": "20000", "qp_status_msg": "Approved",
    "3d_secure_status": "Y", "created_at": "2026-05-06T09:24:14Z" }
]

The handler received a callback for operation 1 only (the 3DS placeholder), and correctly skipped it via the existing check at line 1052:

if (!isAccepted && "30100".Equals(quickPayStatusCode, StringComparison.OrdinalIgnoreCase))
{
    return CheckDataResult.CallbackSucceed;
}

No callback ever arrived for operation 2. The customer returns, StateOk fetches the response above, sees the last operation isn't pending, and gives up.

Case 2 – MobilePay Online (Dankort underneath). API response from StateOk:

"accepted": true,
"operations": [
  { "id": 1, "type": "session",   "qp_status_code": "20000", "acquirer": "mobilepayonline",
    "created_at": "2026-05-06T10:46:31Z" },
  { "id": 2, "type": "authorize", "qp_status_code": "20000", "qp_status_msg": "Approved",
    "acquirer": "nets", "created_at": "2026-05-06T10:46:40Z" }
]

QuickPay's callback was ~3 minutes delayed – when it eventually arrived, the customer had already retried and CheckoutHandlerOrderId had moved on (the late callback then failed with "ordernumber returned from callback does not match"). But again, by the time the customer was redirected back, StateOk already had the response above in hand.

The existing CheckData already handles both cases correctly

CheckData selects the right operation based on whether the order is already complete:

Dictionary<string, object> operation;
if (!order.Complete)
{
    operation = operations.LastOrDefault(op => Converter.ToString(op["type"]) == "authorize");
}
else
{
    operation = operations.Last();
}

For both responses above, LastOrDefault(type == "authorize") returns operation 2 (status 20000, Approved) – exactly what's needed to complete the order. The full authorize flow that follows (SetOrderCompleteSetOrderSucceededCheckoutDone) would all run correctly.

So the missing piece is just that StateOk never feeds the API response into CheckData.

Proposed fix

Run CheckData on the API response as a fallback inside StateOk before printing the error:

lock (lockObject)
{
    if (order.Complete)
    {
        RedirectToCart(order);
        return null;
    }

    // Callback didn't complete the order. Process the payment from the API response;
    // CheckData's LastOrDefault(type == "authorize") will pick the most recent
    // authorize operation, which for 3DS payments is the approved one (status 20000),
    // not the 30100 placeholder.
    var responseText = ExecuteRequest(order, ApiService.GetPaymentStatus, order.TransactionNumber);
    CheckData(order, responseText, order.Price.PricePIP, doCheckSum: false);

    if (order.Complete)
    {
        RedirectToCart(order);
        return null;
    }

    if (Common.Context.Cart == null)
    {
        CheckoutDone(order);
    }
}

doCheckSum: false because the response comes from the API, not from a callback (which would carry the QuickPay-Checksum-SHA256 header). This is the same pattern already used by ProcessPayment and ProceedReturn.

Both scenarios self-heal with this change: by the time the customer returns to the shop, the API already contains the approved authorize operation, and the existing logic in CheckData does the rest.

Has anyone else seen this, and would you consider this fix for the standard handler?

Best regards, Anders


Replies

 
Nicolai Pedersen Dynamicweb Employee
Nicolai Pedersen
Reply

Which version?

 
Anders Ebdrup
Reply

This version:

Dynamicweb.Ecommerce.CheckoutHandlers.QuickPayPaymentWindow.dll 3.1.1+8dd4d3bee9fbfd66f7b4f92f239f7295384c0933

Dynamicweb.Ecommerce.CheckoutHandlers.QuickPayPaymentWindow, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
Build date Fri, 26 Jan 2024 13:19
 
Has this issue been addressed in the later versions?
 
Nicolai Pedersen Dynamicweb Employee
Nicolai Pedersen
Reply

Thanks. We will take a look at it.

 

You must be logged in to post in the forum