Plan payment initialization across guest and account checkout

Model guest and account payment attempts in Framework 4.8, reject stale results, and preserve uncertainty until the provider outcome is verified.

Znode 9GuideAdvancedGCG engineering guide
In this guide
  1. Checkout session
  2. Server-owned order context
  3. Payment initialization
  4. Provider interaction
  5. Verified result
  6. Order placement or recovery
The payment lifecycle keeps authoritative amounts and checkout ownership on the server, then reconciles a verified provider result before order placement.

Establish the payment boundary

Payment initialization is not a browser convenience call. It creates a stateful relationship among the authenticated or guest checkout session, server-owned order context, payment provider, and later authorization or challenge result. The browser may receive a short-lived reference or client configuration that the provider explicitly permits, but it must not decide amounts, billing identity, order ownership, or payment method authority.

Guest checkout needs the same care as account checkout. A guest has no durable account relationship to rely on, so the server must bind the session, cart, contact data, and allowed next step deliberately. An account shopper may also have changed buying context, price eligibility, or address since the cart was created. Recalculate authoritative order data at the approved lifecycle point.

  • Keep raw card data out of application code, logs, and samples.
  • Use only the provider integration model approved for the target release.
  • Treat client-returned references as untrusted until server validation.
  • Do not expose hidden payment-provider failures to shoppers.

Describe states before requests

Add PaymentPolicy.cs to the Znode 9 Framework 4.8 server project. PaymentState names the cart Revision, current Phase, and AttemptId. Begin reserves a new attempt, RecordResult accepts a server-verified provider outcome, Timeout marks uncertainty, and CartChanged decides whether a new cart revision can begin.

Build the revision from every payment-relevant change, including amount, currency, payment method, account, and address. Bind the state to the authorized guest session or account checkout. The model carries application references only; payment amounts, credentials, raw card data, and provider tokens remain with their designated server or hosted-provider components.

Persist each transition only if the durable record's version still matches the version you read. This compare-and-swap check prevents two requests from reserving different active attempts at once. Save the reservation before its provider call, retain old attempts, and enforce unique attempt IDs across the checkout lifetime.

Payments/PaymentPolicy.cs csharp

using System;
using System.Text.RegularExpressions;

namespace Gcg.DeveloperExamples.Payments
{
    public enum PaymentPhase
    {
        Ready, Pending, Challenge, Authorized, Declined, Unknown, Expired
    }

    public sealed class PaymentState
    {
        public string Revision { get; }
        public PaymentPhase Phase { get; }
        public string AttemptId { get; }
        public PaymentState(string revision, PaymentPhase phase, string attemptId = null)
        {
            PaymentPolicy.RequireReference(revision);
            if (!Enum.IsDefined(typeof(PaymentPhase), phase))
                throw new ArgumentOutOfRangeException("phase");
            if (phase == PaymentPhase.Ready && attemptId != null)
                throw new ArgumentException("A ready state has no active attempt.");
            if (phase != PaymentPhase.Ready)
                PaymentPolicy.RequireReference(attemptId);
            Revision = revision;
            Phase = phase;
            AttemptId = attemptId;
        }
    }

    // Persist each transition using compare-and-swap on a durable state version.
    // Provider results enter this policy only after server-side verification.
    public static class PaymentPolicy
    {
        public static void RequireReference(string value)
        {
            if (value == null || !Regex.IsMatch(value, @"\A[A-Za-z0-9_-]{1,80}\z"))
                throw new ArgumentException("Invalid application reference.");
        }

        public static PaymentState Begin(PaymentState state, string revision, string attemptId)
        {
            RequireRevision(state, revision);
            RequireReference(attemptId);
            if (state.Phase != PaymentPhase.Ready && state.Phase != PaymentPhase.Declined &&
                state.Phase != PaymentPhase.Expired)
                throw new InvalidOperationException("Attempt already active.");
            if (state.AttemptId == attemptId)
                throw new InvalidOperationException("Use a fresh attempt.");
            return new PaymentState(revision, PaymentPhase.Pending, attemptId);
        }

        public static PaymentState RecordResult(PaymentState state, string revision,
            string attemptId, PaymentPhase verifiedOutcome)
        {
            RequireAttempt(state, revision, attemptId);
            if (verifiedOutcome != PaymentPhase.Challenge && verifiedOutcome != PaymentPhase.Authorized &&
                verifiedOutcome != PaymentPhase.Declined && verifiedOutcome != PaymentPhase.Expired)
                throw new ArgumentOutOfRangeException("verifiedOutcome");
            if (state.Phase == verifiedOutcome)
                return state;
            if (!IsUnresolved(state.Phase) &&
                !(state.Phase == PaymentPhase.Authorized && verifiedOutcome == PaymentPhase.Expired))
                throw new InvalidOperationException("Attempt is terminal.");
            return new PaymentState(revision, verifiedOutcome, attemptId);
        }

        public static PaymentState Timeout(PaymentState state, string revision, string attemptId)
        {
            RequireAttempt(state, revision, attemptId);
            if (!IsUnresolved(state.Phase))
                throw new InvalidOperationException("Attempt is terminal.");
            return new PaymentState(revision, PaymentPhase.Unknown, attemptId);
        }

        public static PaymentState CartChanged(PaymentState state, string revision)
        {
            if (state == null)
                throw new ArgumentNullException("state");
            RequireReference(revision);
            if (state.Revision == revision)
                return state;
            if (IsUnresolved(state.Phase) || state.Phase == PaymentPhase.Authorized)
                throw new InvalidOperationException("Reconcile or void the attempt before changing cart.");
            return new PaymentState(revision, PaymentPhase.Ready);
        }

        public static bool MayPlaceOrder(PaymentState state, string currentRevision)
        {
            return state != null && state.Phase == PaymentPhase.Authorized &&
                state.Revision == currentRevision && !string.IsNullOrEmpty(state.AttemptId);
        }

        private static bool IsUnresolved(PaymentPhase phase)
        {
            return phase == PaymentPhase.Pending || phase == PaymentPhase.Challenge || phase == PaymentPhase.Unknown;
        }

        private static void RequireRevision(PaymentState state, string revision)
        {
            if (state == null)
                throw new ArgumentNullException("state");
            RequireReference(revision);
            if (state.Revision != revision)
                throw new InvalidOperationException("Stale cart revision.");
        }

        private static void RequireAttempt(PaymentState state, string revision, string attemptId)
        {
            RequireRevision(state, revision);
            RequireReference(attemptId);
            if (state.AttemptId != attemptId)
                throw new InvalidOperationException("Stale attempt.");
        }
    }
}

Starting from Ready at revision 'cart-12', Begin with attempt 'attempt-7' returns Pending. If the provider response is lost, Timeout returns Unknown. Begin is then blocked, and MayPlaceOrder is false. Reconcile the same attempt with the provider; a verified Authorized result enables the payment readiness guard only for cart-12. Before changing an active or authorized cart, reconcile or void the attempt and persist the confirmed expired state.

Use a vendor sandbox and specialist review

Use the installed payment adapter to translate provider-specific requests and verified responses. Znode's custom-payment guide describes work across Payment, API, Admin, and WebStore; its CyberSource guide includes hosted iframe entry and registered-user saved cards. This state policy fits around the selected provider's actual lifecycle.

Exercise guest and account checkout through challenge, cancellation, refresh, duplicate submission, timeout, and external-page return. MayPlaceOrder is one payment readiness check. The order service must still verify checkout authorization, current totals, inventory, and that the order is created only once.

  • Pin the Znode patch and payment package used by the checkout.
  • Keep sandbox credentials in deployment configuration.
  • Retain sanitized provider lifecycle traces for review.
  • Document the reconciliation and cancellation operation for the selected provider.

Build safe observability

Support diagnostics should expose a correlation ID, bounded state, timestamp, and safe reason category. They should not include raw request bodies, billing addresses, authentication values, token references, or downstream headers. Access must be authenticated and limited to the people responsible for resolving payment issues.

A shopper-facing message should say what can safely be known, such as that payment could not be completed and the cart remains available. It should not declare that no charge occurred unless a verified provider result supports that statement.

Ongoing verification

Re-run the state and sandbox scenarios when the provider package, hosted fields, account authentication, or checkout lifecycle changes. Include a changed cart revision after authorization and a delayed response from an earlier attempt. Both must preserve the newer checkout's authority.

References and further reading

Bring your next engineering question.

Need an experienced review of a checkout integration?

Explore how GCG can help

Independent guidance from GCG. Znode is a trademark of its owner. Examples use fictional data and are not official platform documentation. Suggest a correction.

Previous
Previous

Reliable ERP order delivery: retries, idempotency, and replay

Next
Next

Add a database migration and know when DbUp runs it