# Robokassa — the retired direct rail

Robokassa used to settle the **`direct`** (RUB) rail. It was replaced by
[YooKassa](../yookassa) and is now **dormant**: the code, its tests and its wiring are all still
here, but **no deployment sets its credentials**, so `Shops.Configured()` is false, the Result
callback route is never registered, and the direct rail resolves to YooKassa.

The code is kept because the merchant relationship could come back. This file records the operational
knowledge that used to live in `deploy/` and `.gitea/workflows/` and was removed from there so the
live configuration stays free of dead variables.

## How the rail is chosen

`handleWalletOrder` (`backend/internal/server/handlers_intake.go`) resolves the direct rail in this
order:

1. a configured YooKassa shop for the platform channel → YooKassa;
2. otherwise a configured Robokassa shop → **this rail**;
3. otherwise `501 rail_unavailable`.

So reviving Robokassa is a **credentials change, not a code change**: restore the environment block
below and the rail comes back on the next deploy. If both providers are configured, YooKassa wins.

## Environment variables (retired)

The backend still reads all of these. They were removed from `deploy/docker-compose.yml`,
`deploy/.env.example`, `deploy/write-prod-env.sh`, `deploy/README.md` and the three workflows.

| Backend variable (inside the container) | Meaning |
| --- | --- |
| `BACKEND_ROBOKASSA_MERCHANT_LOGIN` | Shop login. Empty ⇒ the shop is dropped. Legacy single-shop form: it seeds the **web** channel. |
| `BACKEND_ROBOKASSA_PASSWORD1` | Pass phrase #1 — signs the outgoing payment request. |
| `BACKEND_ROBOKASSA_PASSWORD2` | Pass phrase #2 — signs and so verifies the incoming Result callback. |
| `BACKEND_ROBOKASSA_TEST` | `1` adds `IsTest=1`, so the shop simulates payments against its **test** pass phrases and no money moves. Empty/`0` is live. |
| `BACKEND_ROBOKASSA_WEB_{MERCHANT_LOGIN,PASSWORD1,PASSWORD2,TEST}` | The per-channel **web** shop; overrides the legacy set above. |
| `BACKEND_ROBOKASSA_ANDROID_{MERCHANT_LOGIN,PASSWORD1,PASSWORD2,TEST}` | The per-channel **android** (RuStore) shop (D42). |

Password3 (Robokassa's JWT-invoice API) was never used.

The three-step naming this repo uses was: a Gitea secret/variable `PROD_BACKEND_ROBOKASSA_PASSWORD1`
→ mapped by the workflow to `ROBOKASSA_PASSWORD1` → mapped by compose to
`BACKEND_ROBOKASSA_PASSWORD1` inside the container.

| Contour | Gitea entries that fed the rail |
| --- | --- |
| test | secrets `TEST_BACKEND_ROBOKASSA_{MERCHANT_LOGIN,PASSWORD1,PASSWORD2}`; `ROBOKASSA_TEST` was hard-coded to `"1"` in `ci.yaml` so the contour could never take real money whatever the shop's own mode. |
| prod | secrets `PROD_BACKEND_ROBOKASSA_{MERCHANT_LOGIN,PASSWORD1,PASSWORD2}`; **variable** `PROD_BACKEND_ROBOKASSA_TEST`, deliberately a variable so go-live was a flag flip rather than a secret rotation. Mirrored in `prod-rollback.yaml` so a rollback did not dark the rail. |

**Known gap, if the rail is ever revived:** the per-channel `…_WEB_*` / `…_ANDROID_*` variables
existed in compose and in `config.go` but were **never** carried by any workflow or by
`write-prod-env.sh` — only the four legacy variables reached prod. A revival that wants the
multi-shop split must add them to the deploy plumbing too.

## Cabinet configuration

Set in the Robokassa ЛКК, per shop:

- **Result URL** — `https://<host>/pay/robokassa/result/<channel>` (`…/web`, `…/android`). The bare
  `…/pay/robokassa/result` is the legacy path and is verified as the **web** shop. Method: POST.
  Each shop's callback is verified only by its own Password2 (`Shops.Verifier` does not fall back).
- **Success URL** — `https://<host>/pay/robokassa/success`.
- **Fail URL** — `https://<host>/pay/robokassa/fail`.
- **Signature algorithm** — SHA-256 (the code hard-codes it; a shop set to MD5 will not verify).
- **54-ФЗ** — fiscalization was cabinet-side through Robokassa's cloud cash register (kassa + ОФД +
  СНО configured by the owner), so no receipt data was ever sent from code. YooKassa does **not**
  work this way: it registers a receipt only if the request carries one.

The gateway routes (`gateway/internal/connectsrv/server.go`) are still registered, so the cabinet
URLs above stay reachable; only the backend has no shop to verify against.

## Protocol notes

- The order id is a UUID and Robokassa's `InvId` is numeric, so the order rides the custom-parameter
  channel as **`Shp_order`** and `InvId` is always sent as `0`. Idempotency at the credit site is
  therefore keyed on the order id, not on a provider payment id.
- Outgoing signature base: `MerchantLogin:OutSum:InvId:Password1[:Shp_key=value…sorted]`.
- Incoming (Result) signature base: `OutSum:InvId:Password2[:Shp_key=value…sorted]`, compared
  case-insensitively.
- The Result callback must be answered with the body `OK<InvId>`, which the gateway echoes verbatim.

## What is *not* reversible by configuration

Ledger rows written while this rail was live carry `provider = 'robokassa'`. That literal is
load-bearing for the `(provider, provider_payment_id)` idempotency index and must never be renamed or
reused for another provider.
