package payments import ( "context" "errors" "net/http" "crussell/internal/square" ) // chargeFailureStatus classifies a SquareClient.CreatePayment error into the // HTTP status a payment handler should return: // // - 503 (Service Unavailable) for AMBIGUOUS failures: transport/network // errors, Square 5xx responses, context cancellation/deadline, and the // retryable 4xx statuses 429 (rate limited), 408 (request timeout), and // 425 (too early) — the money state at Square is unknown, so the frontend // should treat it as a retry (the pending record is resumed on a same-key // retry). Square's own docs treat 429 as "retry later"; mapping it (or a // timeout/early request) to 402 would mislabel a retryable condition as a // permanent decline. // - 402 (Payment Required) for DEFINITIVE declines: a structured Square // error (squareAPIError) carrying any OTHER 4xx status (400/402/422 etc.) // means Square positively rejected the charge (card declined/expired, // AVS/CVV failure) — retrying with the same inputs cannot succeed. // // A nil error is never expected (callers only invoke this on the error path); // it maps to 402 defensively. The dev mock returns plain errors for simulated // failures, which classify as 503 (ambiguous) — correct for a mock standing in // for an unreachable Square. func chargeFailureStatus(err error) int { if err == nil { return http.StatusPaymentRequired } if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) { return http.StatusServiceUnavailable } status := square.ErrorStatusCode(err) if status == 0 || status >= 500 { return http.StatusServiceUnavailable } // Retryable/ambiguous 4xx carve-outs: 429 (RATE_LIMITED), 408 (request // timeout), and 425 (too early) are not definitive declines — Square's // docs tell clients to retry later. Classify them as 503 so the pending // record stays resumable on a same-key retry instead of being labelled a // permanent decline. True declines (400/402/422 etc.) fall through to 402. if status == http.StatusTooManyRequests || status == http.StatusRequestTimeout || status == http.StatusTooEarly { return http.StatusServiceUnavailable } if status >= 400 && status < 500 { return http.StatusPaymentRequired } return http.StatusServiceUnavailable }