AnnouncementsIntroducing Arkoren

    Menu

    Payment Recovery

    Preserve uncertain mutation outcomes and reconcile saved operations within provider-specific replay limits.

    What's on this page
    1. Record requests, keys, merchant scope, and references before recovering interrupted mutations.
    2. Preserve earlier uncertainty even when a later same-key call fails differently.
    3. Reconcile known resources or investigate within provider-specific replay limits.
    On this page

    A provider can accept a payment operation even when your application receives no usable reply. Recovery means finding what happened to that saved operation, not assuming the error permits a new attempt. This applies to checkout creation, preparation, enrollment, completion, refunds, and cancellation.

    Recording Attempts

    Before dispatch, save the authorized application account/order, immutable request, logical operation identity, provider, merchant, and test/live environment. Save the operation key where the request supports one. Cancellation has no portable caller-key field, but still needs an application-owned operation record.

    Coordinate concurrent callers so browser retries, workers, return paths, and notifications do not start independent mutations for one intended effect. Persist every observed resource ID and result, including references returned with an error. A timeout, expired application claim, or failed local save is not evidence that the provider did nothing.

    Keep the exact credential identity when it affects replay scope, without placing the credential itself in ordinary logs. Protect saved approval URLs and client handoff credentials as secrets. Arkoren supplies no durable ledger, transaction coordinator, local billing projection, or background reconciliation worker.

    Preserving Uncertainty

    errors.Is(err, payment.ErrOutcomeUnknown) means a dispatched mutation may have taken effect. Cancellation after dispatch can retain both outcome uncertainty and the caller’s context error. Pre-dispatch cancellation alone has no effect from that call; it says nothing about an earlier call.

    A later same-key rejection or pre-dispatch failure does not resolve an earlier uncertain attempt. Preserve the original request/key and unresolved state. Neither the latest error nor an unavailable read authorizes a fresh-key charge, refund, or fulfillment.

    Model that uncertainty separately from the latest error. For example, an application-owned saved checkout record can include these fields:

    import "arkoren.dev/core/service/payment"
    
    type CheckoutAttempt struct {
        Request   payment.CheckoutRequest
        Checkout  payment.CheckoutID
        Uncertain bool
    }

    This is a storage-model fragment, not an Arkoren persistence API. Store its account/merchant/environment association alongside it. With attempt loaded under that association and errors imported, preserve uncertainty after a call:

    if errors.Is(err, payment.ErrOutcomeUnknown) {
        attempt.Uncertain = true
    }

    Persist that update with the observed references and outcome. Do not assign attempt.Uncertain = errors.Is(err, payment.ErrOutcomeUnknown): a later known failure would erase an earlier unknown effect. Only reconciliation evidence can resolve the saved uncertainty. Preserve context error identity with errors.Is as well; do not infer dispatch state from error text.

    Reading Evidence

    If a resource ID is known, read that resource using its persisted provider, merchant, and environment. GetCheckout, GetPayment, GetRefund, and GetSubscription are reads, not hidden capture or settlement operations. Match observations against the saved target, amount/currency where applicable, and account association before changing application state.

    A succeeded payment can satisfy a matching order’s collection condition; pending/unknown cannot. Refund progress is independent: pending means accepted work is not finalized, not that the mutation outcome is unknown. Subscription lifecycle is not evidence of paid access. Preserve previously observed relationships if a later snapshot omits them.

    Choose bounded current-state reads through your application’s recovery policy, not an unbounded polling loop. A failed or not-found read does not by itself settle an earlier uncertain mutation. A successful HTTP status with an unusable body is not a valid financial observation either.

    If creation returned no ID, the contract has no list/search operation, universal preparation/session lookup, or lookup by operation key or Reference. Correlation text does not make a missing resource discoverable. Use supported provider replay or provider-specific investigation rather than casting another ID.

    Replaying Requests

    Replay only the identical saved request/key within that operation’s documented provider scope and retention. Keep merchant/environment fixed. Do not treat a provider idempotency mechanism as permanent exactly-once processing or assume all operations share its guarantees.

    Stripe and PayPal replay behavior is provider/operation-specific; verify the relevant Stripe idempotency policy or PayPal idempotency policy before choosing a recovery window. The portable contract provides no common retention duration or guarantee that every endpoint supports a replay key.

    Mollie’s POST response cache lasts one hour per exact API credential. Rotating credentials, even within one account, loses that replay scope. Reusing a key after expiry can create a new effect, particularly another partial refund. Retain the credential association and original dispatch timing before considering replay.

    Outside a supported replay window, investigate through the provider’s dashboard or a provider-specific process before authorizing a new attempt. Do not turn missing evidence into permission to retry with a new key.

    Driver-owned clients do not automatically replay mutations. Stripe NewFrom preserves the supplied SDK/HTTP policies instead; configure those deliberately. Custom transports must not add mutation replay independently. No driver performs post-success hydration to fill missing fields, although a documented preflight read can guard currency or lifecycle.

    Classifying Errors

    Use errors.Is/errors.As, not message matching. Classify the current call without erasing the operation’s saved history:

    Error Response
    ErrInvalidRequest, ErrUnsupported Correct input or choose a supported operation; waiting does not fix it.
    ErrUnauthorized, ErrForbidden Fix provider credentials/permissions; buyer approval does not fix provider authorization.
    ErrDeclined Use an application-owned buyer recovery path for this known rejection.
    ErrInvalidState Reconcile lifecycle before considering another mutation.
    ErrRateLimited, ErrUnavailable Apply a bounded provider-aware policy; do not automatically repeat a charge.
    ErrNotFound Recheck the persisted identity/scope; it does not resolve an earlier ambiguous effect.
    ErrOutcomeUnknown Preserve the original operation and reconcile its possible effect.

    For PayPal, malformed successful GET JSON and unusable OAuth replies wrap ErrUnavailable without adding ErrOutcomeUnknown. Semantic GET snapshot failures keep their specific validation/capability errors. For Mollie, malformed or unusable successful GET snapshots wrap ErrUnavailable. Successful mutation decoding failures retain outcome uncertainty in both drivers. A pre-dispatch OAuth failure can leave an earlier same-key attempt unresolved.

    Mollie’s retained native error status can be zero after a successful response that could not be read or decoded. Zero does not prove that no response arrived. Safe driver error messages do not sanitize explicitly unwrapped causes; redact those and borrowed HTTP diagnostics before logging.

    Use Testing to exercise a lost reply followed by a later rejection without moving money. For error identities and operation contracts, see the payment API.

    Open a bulk documentation archive?

    This file contains every guide in its scope, not API declarations. Loading it all into an agent can exhaust its context window and increase processing time and cost. For coding tasks, start with the index and read only the relevant pages.

    Use the full archive for offline indexing, bulk processing, or deliberate large-context use.

    Browse the index instead
    Open archive