Plan payment initialization across guest and account checkout

Treat payment initialization as a server-controlled state transition, with explicit guest, account, amount, currency, and provider-session boundaries.

Znode 9GuideAdvancedGCG engineering guide
In this guide
Checkout session Server-owned order context Payment initialization Provider interaction Verified result 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

Use explicit payment states before making provider calls. The project-owned reducer below rejects a second active attempt, stale cart revisions, and results from another attempt. Its authorized state means a server adapter has already verified the provider response. It carries only application references and phase labels; amounts, credentials, card data, and provider tokens stay in their designated server or hosted-provider boundary.

A timeout is especially important. It can mean the provider did not receive the request, received it and declined it, or accepted it while the response was lost. The correct recovery can require a provider-side status lookup or a controlled operator process. A blind retry can create duplicate authorization attempts or confuse the shopper.

Persist each transition with a version check before triggering its side effect, then record the provider response through the same concurrency boundary. This function supplies deterministic state rules; the durable store serializes competing requests. A cart revision includes every payment-relevant change, including amount, currency, selected payment method, account, and address. An active or authorized attempt blocks cart changes until the provider adapter reconciles or voids it. After confirmed cancellation, persist the corresponding expired state before starting a fresh revision. The attempt ledger must retain old attempts and enforce unique IDs across the checkout lifetime.

payment-state.ts typescript

export type PaymentPhase = "ready" | "pending" | "challenge" |
"authorized" | "declined" | "unknown" | "expired";
export interface PaymentState {
revision: string;
phase: PaymentPhase;
attemptId?: string;
}
export type PaymentEvent =
| { type: "begin"; revision: string; attemptId: string }
| { type: "result"; revision: string; attemptId: string;
outcome: "challenge" | "authorized" | "declined" | "expired" }
| { type: "timeout"; revision: string; attemptId: string }
| { type: "cartChanged"; revision: string };
function reference(value: string): void {
if (!/^[A-Za-z0-9_-]{1,80}$/.test(value))
throw new Error("Invalid application reference");
}
// Server-side helper. Persist using compare-and-swap on the current state version.
// Provider responses enter this reducer only after server-side verification.
export function transitionPayment(
state: Readonly<PaymentState>, event: PaymentEvent
): PaymentState {
reference(state.revision);
reference(event.revision);
if (event.type === "cartChanged") {
if (event.revision === state.revision) return { ...state };
if (state.phase === "pending" || state.phase === "challenge" ||
state.phase === "unknown" || state.phase === "authorized")
throw new Error("Reconcile or void the active attempt before changing cart");
return { revision: event.revision, phase: "ready" };
}
reference(event.attemptId);
if (event.revision !== state.revision) throw new Error("Stale cart revision");
if (event.type === "begin") {
if (state.phase !== "ready" && state.phase !== "declined" &&
state.phase !== "expired") throw new Error("Attempt already active");
if (state.attemptId === event.attemptId) throw new Error("Use a fresh attempt");
return { revision: state.revision, phase: "pending", attemptId: event.attemptId };
}
if (event.attemptId !== state.attemptId) throw new Error("Stale attempt");
if (event.type === "result" && event.outcome === state.phase) return { ...state };
if (state.phase !== "pending" && state.phase !== "challenge" &&
state.phase !== "unknown" &&
!(state.phase === "authorized" && event.type === "result" &&
event.outcome === "expired")) throw new Error("Attempt is terminal");
return {
revision: state.revision,
attemptId: state.attemptId,
phase: event.type === "timeout" ? "unknown" : event.outcome
};
}
export function mayPlaceOrder(state: PaymentState, currentRevision: string): boolean {
return state.phase === "authorized" && state.revision === currentRevision &&
typeof state.attemptId === "string" && state.attemptId.length > 0;
}

Use a vendor sandbox and specialist review

Znode's custom-payment guide describes changes across Payment, API, Admin, and WebStore applications. Its CyberSource guide also describes hosted iframe entry and a saved-card option for registered users. Use the installed provider integration as the transport boundary around this reducer, and map only verified provider outcomes to its events.

Exercise guest and account checkout in the selected provider sandbox. Cover challenge, non-challenge, cancellation, browser refresh, expired session, duplicate submit, timeout, and external-page return. Confirm that a timeout moves the state to unknown, blocks order placement, and triggers reconciliation. mayPlaceOrder is one payment readiness guard; the order service still validates checkout authorization, totals, inventory, and order creation concurrency.

  • 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.