Change evidence report

Provider failure classification: reviewer evidence

PR 1893 distinguishes failures that never reached Stripe or Square from provider credential, response and transport failures. Local deployment faults now produce neutral platform-support guidance and leave the merchant connection health snapshot untouched, while provider-specific failures retain their established operator and customer behavior.

Range
9d8fdc955b1bf705f129ec5ac981fca02f88d911…2512b47d6cb60012ebadf1a1650866efaa5901bc
Source state
fix/1598-provider-failure-classification at code head 2512b47d6cb60012ebadf1a1650866efaa5901bc; this evidence source is the following report update
Generated

Reviewer brief

Outcome and scope

Merchant health is updated only from evidence about the provider connection

classifyProviderFailure now has an explicit local kind. Provider SDK and response evidence is classified before generic local errors, including actual Square timeout classes. API routes and the recheck worker share the same kind and guidance mapping, so a local database or configuration failure cannot deactivate or accuse a healthy merchant connection.

Security and live-boundary posture

The payment provider boundary is exercised with documented SDK error shapes and local PostgreSQL, not live merchant credentials or real money. Independent review required Square SDK precedence, actual timeout-class tests, direct local-failure telemetry, and preservation of provider-owned blockers when a local fault coexists; all are covered in the final candidate.

Claim map

Evidence coverage

Each material changed behavior or state maps to direct evidence or an explicit gap.

Behavior or claimEvidenceStatus
Failures that never reach a provider are classified as localClassifier tests for plain Error, connection pool and local configuration failuresCovered
Square and Stripe transport or response evidence wins over the generic local fallbackDocumented SDK-shape tests, including SquareError, SquareTimeoutError, the Square V1_ERROR/Unknown transport fallback, and Stripe credential/timeout examplesCovered
Local-only failures do not mutate merchant health; local evidence never drives the mutationReal PostgreSQL payment-health API and worker recheck tests cover local-only faults and mixed local/provider blockersCovered

Observed change

Review evidence

Evidence is grouped by the reviewer question each item addresses.

Classifier precedence

Table

Failure evidence maps to one explicit kind

Provider-owned evidence is evaluated before the local fallback so subclassed SDK errors remain provider failures.

Input evidenceCandidate kindOperational meaning
Plain Error or local database/configuration failurelocalThe provider was not reached; inspect the Sauna platform
Stripe invalid-key responsecredentialMerchant provider credentials need attention
Stripe timeout or connection errortransientProvider transport failed; retry guidance remains appropriate
SquareError carrying response metadatarejectionThe provider response is request-specific, not account health
SquareTimeoutErrortransientActual SDK timeout class is not mistaken for local
Unshaped Square Error without SDK or response evidencelocalDo not infer merchant blame from a provider label alone

Health mutation

Table

Local failures leave stored connection health unchanged

The API and worker distinguish the inability to perform a check from evidence that the provider connection is unhealthy.

BoundaryInjected conditionObserved result
Payment connection health APILocal database failure before provider evidenceConnection remains active and last verified snapshot is retained
Recheck workerHealth query throws locallyFailure is logged for platform operators; merchant health is not replaced
Recheck workerProvider onboarding reports account cannot accept chargesConnection is marked unhealthy with provider-owned blocker

Consumer guidance

Table

Every classifier consumer handles local explicitly

Checkout, membership, refunds, point of sale, widget account recovery, health activation and the worker use the shared failure contract.

Consumer familyLocal behaviorProvider behavior retained
Checkout and membershipNeutral temporary platform-support messageCredential and transient provider guidance remains specific
Payment methods and refundsNo merchant reconnect instruction for local faultsProvider rejection and credential remediation remains available
Health activation and recheckDo not overwrite merchant healthReal provider blockers still update health
Widget account recoveryDo not classify an unknown local failure as provider downtimeSquare SDK timeout and response evidence remains transient or rejected

Classifier implementation

Code

Square evidence is shape-checked before the local fallback

The classifier recognizes documented SDK timeout, transport-fallback and response-bearing shapes. Plain or primitive failures stay local.

Final Stripe/Square classifier with decisive return branchesAfter
packages/api/src/lib/payment-connection-health.ts · 2512b47d6cb6 · lines 135-176
export function classifyProviderFailure(
  provider: PaymentProvider,
  error: unknown,
): ProviderFailureKind {
  switch (provider) {
    case "stripe":
      return classifyStripeFailure(error);
    case "square": {
      if (typeof error !== "object" || error === null) return "local";
      const name = "name" in error ? error.name : undefined;
      if (name === "SquareTimeoutError") return "transient";

      const statusCode = "statusCode" in error ? error.statusCode : undefined;
      const errors = "errors" in error ? error.errors : undefined;
      const hasSquareResponseErrors =
        Array.isArray(errors) &&
        errors.some(
          (entry) =>
            typeof entry === "object" &&
            entry !== null &&
            ("code" in entry || "category" in entry),
        );
      // Fern wraps a non-timeout, non-status transport failure in SquareError
      // with the SDK's exact fallback error pair and an optional cause.
      const hasUnknownTransportFallback =
        typeof statusCode !== "number" &&
        Array.isArray(errors) &&
        errors.some(
          (entry) =>
            typeof entry === "object" &&
            entry !== null &&
            "category" in entry &&
            entry.category === "V1_ERROR" &&
            "code" in entry &&
            entry.code === "Unknown",
        );
      if (hasUnknownTransportFallback) return "transient";
      return typeof statusCode === "number" || hasSquareResponseErrors
        ? "rejection"
        : "local";
    }
  }

Worker health preservation

Code

Local recheck faults are raised without erasing provider evidence

A local-only fault exits before any row mutation. If provider blockers coexist, the worker records those blockers but deliberately omits the contaminated activation snapshot.

Local-only failures exit before recordConnectionHealthAfter
packages/worker/src/jobs/recheck-payment-connection-health.ts · 2512b47d6cb6 · lines 273-290
  // A local process or configuration failure is not connection health. Keep
  // the merchant row and its last verified snapshot untouched, but raise it to
  // platform operators instead of suggesting that the studio reconnect.
  const internalFaults = readiness.blockers.filter(
    (blocker) => blocker.code === "internal",
  );
  if (internalFaults.length > 0) {
    logger.error(
      "Payment connection health check failed inside this deployment",
      {
        job: JOB_NAME,
        brandId: brand.id,
        provider: connection.provider,
        blockers: internalFaults.map((fault) => fault.message),
      },
    );
    if (faults.length === 0) return;
  }

Worker health preservation

Code

Mixed failures keep provider blockers and reject the local snapshot

The stored error remains provider-owned, while activationCheck is written only when no local fault was present.

Provider blocker mutation with snapshot guardAfter
packages/worker/src/jobs/recheck-payment-connection-health.ts · 2512b47d6cb6 · lines 315-335
  const recorded = await recordConnectionHealth(db, {
    connectionId: connection.id,
    brandId: connection.brandId,
    provider: connection.provider,
    lastError:
      faults.length === 0
        ? null
        : faults.map((fault) => HEALTH_FAULT_MESSAGES[fault.code]).join(" "),
    // Overwritten whenever the provider answered: a snapshot of a failing
    // account is the thing whoever triages this needs, and leaving the
    // activation-era one in place is precisely the misdirection #1542 is
    // about. Omitted when the check never got far enough to build one (a
    // credential the provider refuses outright), because the stored snapshot
    // is then stale but true, and dropping it would also drop the
    // `providerCurrency` that holds the brand's currency read-only while this
    // connection is live.
    ...(readiness.check && internalFaults.length === 0
      ? { activationCheck: readiness.check }
      : {}),
    verifiedIdentity: connection,
  });

Local diagnostics

Code

Local failures reach console and active tracing

Sanitized trace diagnostics are recorded for the deployment fault. Only proven credential failures mutate connection health.

Telemetry and credential-only health mutationAfter
packages/api/src/lib/payment-connection-health.ts · 2512b47d6cb6 · lines 453-468
  if (kind === "local") {
    const span = trace.getActiveSpan();
    span?.setStatus({ code: SpanStatusCode.ERROR });
    recordSanitizedException(span, params.error);
  }

  if (kind === "credential" && params.connectionId) {
    const label = params.provider === "stripe" ? "Stripe" : "Square";
    await recordConnectionHealth(db, {
      connectionId: params.connectionId,
      brandId: params.brandId,
      provider: params.provider,
      lastError: `${label} refused these credentials while ${params.operation}. Reconnect ${label} in Settings. [ref: ${errorId}]`,
      onlyIfUnmarkedSince: params.resolvedAt,
    });
  }

Confidence

Validation

Pass

Provider classifier and local diagnostics

25 passed, 0 failed, 38 assertions

Pass

Membership lifecycle and cancellation route

29 lifecycle tests and 16 widget cancellation route tests passed, including resource_missing passthrough and local 503 behavior

Pass

Payment health API

4 passed, 0 failed, 17 assertions against local PostgreSQL

Pass

Recheck worker

12 passed, 0 failed, 32 assertions against local PostgreSQL, including local-only and mixed provider/local failures

Pass

API and worker typechecks

Both package typechecks exited successfully on the rebased candidate

Pass

Independent review

Final review found no material defect; follow-up findings for local telemetry and mixed blockers were implemented and rechecked

Limits

Known issues, risks, and gaps

The safer fallback avoids merchant blame without weakening classification backed by provider SDK or response evidence

Unknown unshaped Square errors now require platform investigation An Error carrying neither a documented Square SDK class nor response evidence is local by design. This may defer a novel provider failure to platform support, but avoids deactivating a merchant connection on speculation.