# cp10 Decline-Codes Investigation & Golden-Case Map

> **Status:** spec/investigation artifact (REQ-565, UR-196). No production code. This document is the
> **input to REQ-566** (replace substring `getErrMsg()` with explicit decline codes) and **REQ-569**
> (unit tests on cp10 modules). It is the golden master that REQ-566's refactor must not regress.

## TL;DR

1. **No stable structured decline code is surfaced to the cp10/cp9 frontend today.** The engine
   switches on the **raw message string** (`res.includes(...)`), and the test backend forwards
   Braintree's `errors[].message` text verbatim without extracting `processorResponse.code`. So
   REQ-566 must build an explicit `DECLINE_CODES` enum by **normalising message strings**, keeping the
   substring matcher as a documented internal fallback. (See [Structured-Code Finding](#structured-code-finding).)
2. The **golden-case map** below captures every existing `getErrMsg` branch → exact banner, verbatim.
   REQ-566 is a refactor that must reproduce this table 1:1 (proven by golden-master tests).
3. The **representative trigger strings are source-derived, not live-captured** — the verbatim
   live-response capture for all 9 scenarios is an **operator task** (VPN + merchant key + the card
   path that `?localBackend=go` does not implement). See [Operator follow-up](#operator-follow-up).

## Source of truth

| Concern | File:line |
| --- | --- |
| Decline → banner logic (`getErrMsg`) | `../src/index.js:1807` (forked from cp9 `checkout-eet-digital-inline-form-cp9/src/index.js:1807`) |
| Error consumer (`handlePurchaseError`) | `../src/index.js:1781` |
| No-`responseText` default banner | `../src/index.js:1785` |
| Existing-customer error path (separate, fixed string) | `../src/index.js:1844` |
| Backend forwarded error shape | `utilities/cp9-test-backend/src/go-api/internal/api/create_subscription.go:91` |
| Braintree error model (`APIError`, `graphQLError`) | `utilities/cp9-test-backend/src/go-api/internal/braintree/client.go:56,65,71` |

## Golden-case map (the no-regression contract)

`getErrMsg(res)` runs ordered `res.includes(...)` checks; **first match wins**. Banner strings are
transcribed **verbatim** — preserve every character (phone numbers, apostrophes, "clicking here").

| # | Proposed `DECLINE_CODES` | Trigger substrings (in `res`, OR-ed) | Exact banner returned today |
|---|---|---|---|
| 1 | `AVS_MISMATCH` | `AVS check failed` | `Error purchasing subscription, please enter the correct billing zipcode for your card.` |
| 2 | `PREPAID_UNSUPPORTED` | `Prepaid cards are not supported` | `Sorry we don't accept prepaid card in general. But if you are using a social security card or your case is special, please call us at 833-699-1888.` |
| 3 | `DO_NOT_HONOR` | `Do Not Honor`, `No Account`, `Your card number is incorrect`, `No Such Issuer` | `The transaction has been declined. Your credit card company may be protecting you from fraud. Please make sure that you enter the correct cardholder name, card number, expiration date, and CVV. If the problem continues, please call the customer service number on the back of your card for more information.` |
| 4 | `CARD_RESTRICTED` | `Call Issuer`, `Issuer or Cardholder has put a restriction` | `The transaction has been declined due to this card being reported as lost or stolen. Please call the customer service number on the back of your card for more information.` |
| 5 | `INSUFFICIENT_FUNDS` | `Insufficient Funds`, `insufficient` | `The transaction was declined. You may have reached your credit limit or you may not have sufficient funds in your account. Please check, then try again. You can also choose a flexible billing cycle by clicking here.` |
| 6 | `GENERIC_DECLINED` | `Declined`, `declined`, `Limit Exceeded` | `The transaction has been declined, possibly because of an incorrect card number entry, a high level of recent activity, reaching your credit limit, or insufficient funds. Please call the customer service number on the back of your card for more information.` |
| 7 | `CARD_EXPIRED` | `Expired`, `expired` | `The transaction has been declined because your credit card has expired. Please try again using another payment method.` |
| 8 | `PAYPAL_DECLINED` | `alternative payment method from the customers PayPal wallet` | `The transaction has been declined by PayPal. Please try another payment method.` |
| 9 | `UNKNOWN_DECLINE` (else) | *(no substring matched)* | `The transaction has been declined, please verify that your payment details are correct. If you need further assistance, please call us at 833-699-1888.` |

**Two banners that do NOT pass through `getErrMsg` (document, but keep out of the enum):**

| Path | Condition | Exact banner |
|---|---|---|
| `handlePurchaseError` pre-check (`:1785`) | `error.responseText` is falsy (network/transport error — no body to match) | `Unable to checkout, please verify that your payment details are correct.` |
| `purchaseForExistingCustomer` catch (`:1844`) | any error on the existing-customer create path | `Unable to checkout, please try a different payment method or contact our customer service.` |

## Match-order pitfalls (why the substring chain is brittle — REQ-566's motivation)

- **Order-sensitive.** `Insufficient Funds` (#5) is tested **before** `Declined` (#6). A body of
  `"Declined: Insufficient Funds"` → #5, not #6. Any reordering in REQ-566 silently changes banners —
  the golden-master tests are the guard.
- **The PayPal-400 wrong-banner incident.** A PayPal decline returned as **HTTP 400** whose body does
  **not** contain the exact magic phrase `alternative payment method from the customers PayPal wallet`
  falls through to `UNKNOWN_DECLINE` (#9, the generic banner) instead of `PAYPAL_DECLINED` (#8). This
  is the concrete bug `## Why` cites. REQ-566's explicit-code layer must classify PayPal failures by a
  reliable signal (e.g. payment-method type / `braintree_status` + reason), not this one fragile phrase.
- **Substring over-reach.** `insufficient` (lowercase, #5) matches any body containing the token —
  including unrelated prose. `Expired`/`expired` (#7) likewise. Explicit codes remove this surface.
- **Redundant check.** Branch #3 lists `No Account` **twice** (`../src/index.js:1813`). Harmless but
  dead — recorded in this REQ's Discovered Tasks for REQ-566/REQ-581 cleanup.

<a id="structured-code-finding"></a>
## Structured-Code Finding (resolves the Open Question — D-01)

**Question:** does the BFF/Braintree response expose a stable structured decline code to switch on?
**Answer (source-derived):** **No — not as surfaced to the frontend.**

- The frontend never reads a code: `getErrMsg` switches on `res` (the stringified `responseText`) via
  `includes`. If a structured code were available and trusted, the engine would key on it.
- The Go real-Braintree backend models the error as `graphQLError { message }` only
  (`internal/braintree/client.go:56`) and returns `APIError.Error()` = the joined Braintree
  `errors[].message` strings (`:71`). It does **not** parse or forward `processorResponse.code` /
  `legacyCode`. The JSON the engine receives is `{status:"failed", error:<message text>,
  braintree_error:true, braintree_status:<int>}` (`create_subscription.go:91`).
- Braintree's GraphQL API *can* expose structured fields (`processorResponse { legacyCode, code }`),
  but capturing/forwarding them is **backend work outside this UR's scope**. Treat the upstream as a
  message-string source for now.

**Therefore REQ-566 takes the "Also:" design:** an explicit `DECLINE_CODES` enum keyed on canonical
reasons, derived by normalising the message strings in the table above, with the legacy substring
matcher retained as a documented internal fallback for `UNKNOWN_DECLINE`. (Already REQ-566's framing —
no new follow-up REQ required.)

<a id="operator-follow-up"></a>
## Operator follow-up (verbatim live capture — optional refinement, not a blocker)

The trigger substrings above are **derived from the engine's own matcher**, which is the contract that
matters for a no-regression refactor. To additionally pin the **exact upstream message strings** per
scenario (useful if REQ-566 wants to assert on real bodies), an operator with VPN + the merchant
`24z644xq` key should, once the card path exists:

1. Drive each scenario (AVS failure, Do Not Honor, Insufficient Funds, Call Issuer/lost-stolen,
   Expired, prepaid-not-supported, PayPal-declined, generic decline, PayPal 400) against the real
   sandbox.
2. Record the verbatim `responseText` / error object for each.
3. Append the captured strings to this table so REQ-569's golden-master tests can assert on real bodies
   rather than representative substrings.

Until then, REQ-566/REQ-569 test against the **substring → banner** contract documented here, which is
sufficient to prove no regression of the existing user-facing behaviour.

## Related

- [prime-cp9](../../cp-eet_9-layout-update/cp9-prime-docs/prime-cp9.md) — cp9 page (decline-handling provenance)
- [prime-cp9-test-backend](../../../../../../utilities/cp9-test-backend/prime-cp9-test-backend.md) — the backends that forward these errors
- REQ-566 (consumer: explicit codes), REQ-569 (consumer: unit tests), REQ-571 (cp10 engine area-index prime)
