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.
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 claim
Evidence
Status
Failures that never reach a provider are classified as local
Classifier tests for plain Error, connection pool and local configuration failures
Covered
Square and Stripe transport or response evidence wins over the generic local fallback
Documented SDK-shape tests, including SquareError, SquareTimeoutError, the Square V1_ERROR/Unknown transport fallback, and Stripe credential/timeout examples
Covered
Local-only failures do not mutate merchant health; local evidence never drives the mutation
Real PostgreSQL payment-health API and worker recheck tests cover local-only faults and mixed local/provider blockers
Covered
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 evidence
Candidate kind
Operational meaning
Plain Error or local database/configuration failure
local
The provider was not reached; inspect the Sauna platform
Stripe invalid-key response
credential
Merchant provider credentials need attention
Stripe timeout or connection error
transient
Provider transport failed; retry guidance remains appropriate
SquareError carrying response metadata
rejection
The provider response is request-specific, not account health
SquareTimeoutError
transient
Actual SDK timeout class is not mistaken for local
Unshaped Square Error without SDK or response evidence
local
Do 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.
Boundary
Injected condition
Observed result
Payment connection health API
Local database failure before provider evidence
Connection remains active and last verified snapshot is retained
Recheck worker
Health query throws locally
Failure is logged for platform operators; merchant health is not replaced
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
// 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
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
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.