Memory Appendix
Octopus Self Vendor
OCTO vendor — Octopus consuming its own /api/v1 gift-card API as a federated voucher vendor
Source memory file:
project_octopus_self_vendor.md· Category: Project / investigation This is a verbatim dump of Claude's persistent memory for the Octopus project. Rendered inside a code block so nothing is altered.
---
name: project_octopus_self_vendor
description: OCTO vendor — Octopus consuming its own /api/v1 gift-card API as a federated voucher vendor
metadata:
node_type: memory
type: project
originSessionId: d4bab5ea-0913-4485-a34e-39578e6e3198
---
Added voucher vendor **OCTO** ("Octopus Network"): one Octopus instance sources vouchers from a federated, Octopus-compatible `/api/v1` endpoint (docs at developer.octopuscards.io) as if it were any third-party supplier. Async + webhook, login-based auth.
Files: `services/external_vendors/vouchers/octopus_vendor.go` (+ `octopus_types.go`), registered in `factory.go`; seeder `database/seeder/octopus_vendor_seeder.go` (creds in vendor_attributes like all vendors — defaults to public sandbox); tests `octopus_vendor_test.go` (httptest unit) + `octopus_sandbox_test.go` (`//go:build sandbox`, factory-built from attributes, read-only by default, order-create gated behind `OCTOPUS_SANDBOX_ALLOW_ORDER=true`).
Non-obvious contract facts verified against the live sandbox (`sandbox-api.octopuscards.io`, demo client id 1):
- Login `POST /auth/login {username,password}` → `{data:{access_token,refresh_token,access_expires_at(~1h),refresh_expires_at(~7d),client_id},success:true}`. Adapter caches tokens, refreshes early (60s skew), re-auths once on 401.
- **List endpoints return BARE JSON arrays** (products, wallets, orders-list), while login/create return `{data:…}` envelopes. Order detail parsed defensively (envelope-or-bare). Errors are `{error:{code,message,name}}`.
- `client_reference` is the upstream idempotency key, never echoed back in a response body. GenerateReferenceCode returns a STABLE ref (no retry suffix). **Recovery uses the EXISTING list endpoint `GET /api/v1/orders?client_reference=REF`** (exact-match, client-scoped; already supported by [handler.go parseQueryParams](http/handler/handler.go) → repo GetOrders Conditions["client_reference"], verified live: unknown ref → `200 []`). NO new API was added — user was explicit: existing API with query param only, no new endpoints. Adapter `GetOrderStatus(nil, ref)` → list-by-reference: hit → Success + OrderID (orchestrator resumes); empty/404 → ErrOrderNotFound (orchestrator creates fresh order, same ref). CreateOrder's 400 "Duplicate client_reference" handling remains a safety net.
- Products: fixed page size 50 (per_page ignored), paginate via `X-Total-Pages`/`X-Has-More` headers; each product's `available_denominations[]` expands to one vendor_product per denomination (fixed → min==max). Availability `POST /api/v1/products/:id/availability {denomination,quantity}` → `{is_available:bool}`.
- Webhook verification is FREE: generic `HMACWebhookVerifier` (X-Signature = hex HMAC-SHA256(body, webhook_secret)) auto-dispatched by `services/external_vendors/webhooks/factory.go` for any non-Runa vendor with a `webhook_secret` attribute. Confirmed byte-for-byte identical to our OUTBOUND signer `services/webhook_sender.go SignWebhookPayload`. The webhook signing key is a vendor attribute (`webhook_secret`) like the login creds — set it to the SAME value as the upstream client's webhook signing token and verification activates; blank ⇒ NoopVerifier (unverified). Sandbox issues no secret.
- Webhook order matching is now **vendor-scoped**: `services/vendor_webhook_service.go processOrderWebhook` filters order items by `{vendor_order_id, vendor_id}` (was vendor_order_id only) — required because OCTO's small integer order ids collide across vendors.
- **Orphan recovery at webhook time (#3)**: outbound webhook carries `data.client_reference` (renamed from `data.ref`). Adapter surfaces it as `NormalizedWebhookPayload.ResourceReference` → `entity.VendorWebhookRequest.ResourceReference`; processOrderWebhook falls back to matching `vendor_reference_code` (vendor-scoped) when the vendor_order_id lookup misses, then schedules retry (which re-binds vendor_order_id via reference recovery). `ResourceReference`/`ResourceReference` are additive optional fields on the shared contract.
- FAILED/CANCELLED upstream is terminal — adapter never sets ShouldReCreate (recreating would hit the stable client_reference as a duplicate).
- **Self-loop guard**: `NewOctopusVendor` errors if host equals our own base URL (`APP_BASE_URL` env or `self_base_url` in Extra) — point OCTO at a *different* instance.
## OCTO_TOPUP + OCTO_ESIM (federated topup & eSIM vendors)
Same federation pattern extended to direct-topup and eSIM as **separate vendor rows** `OCTO_TOPUP` (VendorType DIRECT_TOPUP) and `OCTO_ESIM` (VendorType ESIM), mirroring DTONE/DTONE_ESIM. Both async+webhook, login-token auth, creds in vendor_attributes.
Shared helpers in `services/external_vendors/octopuscommon/`: `SelfLoopGuard(host, selfBase)` + `NormalizeHostForCompare`; `TokenManager` (login→cache→refresh-before-expiry→single-flight→Invalidate-on-401, with `FuzzParseAuthResponse`). Adapters bridge the token into `vendorhttp` via a `bearerAuth` `AuthProvider` (atomic.Pointer[string], race-safe swap) and a `doAuthed` wrapper that re-auths once on 401 — **vendorhttp returns a non-nil resp alongside err on non-2xx, so check `resp.StatusCode==401` BEFORE returning on err**.
- **Topup** `services/external_vendors/direct_topup/octopus/` (octopus_vendor.go + types.go). `VendorVariantID` is a composite `"productID:variantID"` (upstream create takes parent product_id+amount and auto-selects variant; `upstreamProductID()` parses the prefix). CreateOrder: 2xx→parse; 400-duplicate→`ErrDuplicateMerchantRef`; 5xx/429/transport→leave Status empty (PENDING, retry); other 4xx→terminal FAILED. GetOrderStatus by id or recovery via `GET /api/v1/topups/orders?client_reference=`. Registered in `direct_topup/factory.go`.
- **eSIM** `services/external_vendors/esim/octopus/` (octopus_esim_vendor.go + types.go). Same composite PlanID, same create classification. Registered in `esim/factory.go`. **Key airtight decision:** our own outbound eSIM webhook NEVER carries the activation_code (single-use credential, omitted by `webhook_service.go TriggerEsimEvent`). So `ParseWebhook` maps `esim.delivered`→**PROCESSING** (a non-terminal nudge that ProcessEsimWebhook reduces to PENDING — no regression, terminal short-circuit guards it), and the **cron poll** `GET /api/v1/esim/orders/:id` (which DOES return activation_code) is the authoritative completion path. Only `esim.failed`/`esim.cancelled` are terminal via webhook; lifecycle events (installed/activated/depleted/unknown) are rejected so they can't regress an order. `LookupCreditPartyStatus` is a graceful no-op (Success=false, nil err). `ShouldSyncPlans()=false` (catalog seeded directly, not pulled). eSIM webhook needs BOTH `webhook_secret` (HMAC) AND `webhook_path_token` (unguessable URL segment — handler 401s on blank/mismatch).
Seeders: `octopus_topup_vendor_seeder.go`, `octopus_esim_vendor_seeder.go` (registered in runner.go); orchestration seeders `octopus_topup_orchestration_seeder.go` + `octopus_esim_orchestration_seeder.go` (NOT auto-registered — invoked directly in E2E setup; composite VendorVariantID `"1:1"`).
## Track B test harness (`test/octopusfake/`)
In-process programmable upstream (httptest, NOT mocky-balboa): auth/wallets handlers, chaos engine (`Rule{Method,PathContains,Mode once/count/always/prob, Status,Delay,Drop,Malformed}`), request recorder, `SignWebhook`. **Order engines** (`EnableTopupOrderEngine`/`EnableEsimOrderEngine` + `SetTopupBehavior`/`SetEsimBehavior(create,poll,failure[,activation,iccid])` + `Reset()`): in-memory order lifecycle keyed by client_reference, dup-on-re-POST→400, async PENDING→DELIVERED on first poll. eSIM engine renders activation_code/iccid only on a delivered GET (mirrors webhook-never-carries-code). Bind via `testhelpers.BindVendorToOctopusFake(t,repo,code,fake.URL,fake.WebhookSecret)`.
E2E suites (`//go:build orchestration`, real committed Postgres): `test/clientapi/octopus_topup_orchestration_test.go` + `octopus_esim_orchestration_test.go` — happy/async-poll/webhook/chaos/idempotent/bad-sig + concurrency (N cron ticks → refund once; cron-vs-webhook race) + eSIM bad-path-token. Per-run nonce on client_reference (committed DB). **Webhook body must be sent with `testhelpers.WithRawBody` not `WithBody`** (WithBody JSON-marshals → base64 → signature mismatch). Adapter packages have `goleak.VerifyTestMain` + `-race` soak tests (32 concurrent workers, sprinkled 401s).
See [[feedback_vendor_chaos_and_tests]], [[feedback_no_vendor_in_customer_facing]] (OCTO is admin-only naming), [[feedback_repo_test_coverage]], [[feedback_no_webhooks_in_flight_substatus]].