OctoWiki

Known Issues & Triage

The single prioritized index of every bug, security gap, dead-code smell, and doc discrepancy surfaced while writing this wiki — grouped by severity, each linked to the detail page. Start here.

Every issue found while documenting the codebase, in one place. Each row links to the page with the full context. Nothing here is speculative — all were confirmed against source with file:line citations on the linked pages.

How to read this

Severity reflects blast radius if left unfixed, not effort to fix. 🔴 = money loss, data corruption, cross-tenant leakage, or a live security hole. 🟠 = wrong behavior / missing safety rail with a bounded impact. 🟢 = dead code, stale comments, or doc drift — safe to defer, cheap to clean.

🔴 Critical — money, correctness, tenancy, security

IssueAreaWhereDetail
Payout pipeline wired but not drivenPOST /payouts debits the wallet then stops at created; processor/status-sync crons unregistered, QueuePayout uncalled → nothing ever reaches Merit/LedigPayoutsmain.go:227, payout_service.go:391Payouts
No payout refund path — wallet debited but never re-credited on failure/cancel (fund loss)Payoutspayout_service.go:518Payouts
Shopify writes client_id = 0 on every ingest insert (order/customer/item) → FK failure or cross-tenant mis-attributionShopifyfetch_shopify_orders_job.go:258/368/448Shopify
Shopify fulfilment-failure persists status DELIVERED → Shopify order stuck unfulfilled forever, excluded from retryShopifyprocess_pending_shopify_orders_job.go:291/335Shopify
services/wallet_refund.go is dead — refunds re-implemented inline in handlers; two parallel impls, tests pin the unused oneWalletwallet_refund.go vs handlersWallet & Ledger
Top-up webhook not concurrency-safe — no row-lock, no terminal short-circuit, no auto-refund on FAILED → status regression + un-refunded failuresTop-upvendor_webhook_service.go:282Top-up & eSIM
All-FAILED voucher order reads PENDING, never refundsgetOrderDeliveryStatus maps "all items failed, none delivered" to OrderPending not OrderFailed; failed items are excluded from the next retry filter → funds stay debited, order trapped until an admin cancels/refundsOrderscreate_voucher_order.go:437, :2251Troubleshooting
Dead supplier leaves recharges/eSIM PENDING forever, un-refunded — the transient-failure branch no longer auto-FAILs on max-retry (removed as conflicting with the recreate budget), so a down supplier loops on backoff with money debited until the recreate budget (5) parks it for admin; no circuit breaker on the top-up/eSIM pathTop-up/eSIMservices/esim_order.go:826, :981Troubleshooting
Admin cancel/reset never release inventory — allocated stock stays ALLOCATED forever (leak); reset hard-deletes the referencing order_items → orphaned beyond recovery; the pool member was already ZREM'd so it's double-lostAdmin/Inventoryadmin_ui.go:2668/3248, inventory.go:1485Inventory Flow, Admin Order Actions
Payout engine designed but deadpayout-processor + payout-status-sync crons never registered, QueuePayout uncalled → only created+cancelled reachablePayoutsmain.go:227State Machines, Cron Catalog
Admin cancel flags REFUNDED even when the wallet credit fails (best-effort) → un-refunded-but-flagged orders that then block the Refund endpointAdmin/Walletadmin_ui.go:2745 vs :2825Admin Order Actions
CSRF disabled — middleware defined but commented out; admin panel relies solely on SameSite=StrictAuthmain.go:513Auth findings
No rate limiting anywhere — none on OTP/2FA, login, forgot-password, or refreshAuthcodebase-wideAuth findings
TOTP secrets stored plaintext at rest (backup codes are hashed; seeds are not)Authrepo/client_user.go:485, repo/admin_user.go:428Auth findings
JWT secret hardcoded fallback "your-super-secret-key" when JWT_SECRET unset — no startup assertAuthutils/jwt.go + 4 sitesAuth findings
Committed secrets — root .env (Twilio/SendGrid/OpenAI/JWT/APP_KEY), wrangler.jsonc (ENCRYPTION_KEY/ADMIN_TOKEN/…), settings.local.json (sandbox PGPASSWORD), compose defaultsSecrets.env:16-23, wrangler.jsonc:27,156, docker-compose.yamlSecrets & Config, Deploy
SEAGM logs secret_key in plaintext at Info level ("TEMP DIAGNOSTIC")Vendorseagm adapterSEAGM
Grasshopper deriveKey is not a KDF — pads/truncates the raw key to 32 bytes (no salt/PBKDF2); no key rotation/versioning so rotating ENCRYPTION_KEY orphans all D1 ciphertextGrasshoppercrypto.ts:9Grasshopper
Grasshopper admin auth has no rate limitingADMIN_RATE_LIMITER declared (wrangler comment claims brute-force protection) but never calledGrasshopperadmin-auth.ts:6Grasshopper
All vendor credentials stored plaintextvendor_attributes.value TEXT has no encryption, no is_secret, no masking; the payout table's is_secret flag is ignoredVendor/Secretsvendor_attributes.goVendor Config

🟠 High / Medium — wrong behavior, missing safety rails

IssueAreaWhereDetail
"Automation blocker" middleware doesn't exist — CLAUDE.md + route comments reference a production control with no implementationAuthroutes.go:60Auth findings
Per-currency precision bug — all money forced to 2 dp; currencies.precision ignored (KWD/BHD/JPY mis-round); ledger forex_rate NUMERIC(8,2) truncates/overflowsWalletutils/money.go, migrationsWallet & Ledger
created_by silently dropped in the debit stored proc — user attribution always NULLWallet…for_fx.sql INSERTWallet & Ledger
Wallet webhook fires pre-commit from a goroutine → can emit wallet.debited for a rolled-back txWalletwallet_debit.go:129Wallet & Ledger
Race-loser insufficient-funds surfaces as raw pg error, not ErrInsufficientBalanceWalletrepo/wallet.go:465Wallet & Ledger
TriggerPayoutEvent has zero callers → payout client webhooks never firePayoutspayout_webhook_service.go:41Payouts
Beneficiaries need an admin-linked provider or CreatePayout fails resolution silentlyPayoutsbeneficiary-create pathsPayouts
OrderNotifier unwired → voucher partial/cancelled emails never sent (templates exist)Notificationsservices/order_notifier.goNotifications
SMTP transport drops attachments — bulk-voucher XLSX not delivered under EMAIL_PROVIDER=smtpNotificationssmtp.go:210Notifications
Shopify orders mis-tagged source=APISourceShopify constant deadShopifycreate_voucher_order.go:804Shopify
Shopify: no rate-limit / query-cost handling — no backoff on THROTTLEDShopifyclient.go:131Shopify
eSIM inbound webhook has no idempotency-key row — relies solely on terminal-state lock; a non-terminal duplicate could double-processeSIMesim_webhook.go:137Top-up & eSIM
Inventory expiring >1yr out is pumped but never allocatable — pump admits expires>=now+30d (no upper bound) but allocation caps at now+365d → paid-for stock strandedInventoryinventory.go:672 vs :118Inventory Flow
Allocation has no DB fallback on cache miss — empty/unpumped pool → treated as insufficient inventory even when DB has AVAILABLE rowsInventorycreate_voucher_order.go:1723Inventory Flow
G2A concurrency loser gets 500 (not a clean 200/409) — safe (no double-mint) but roughG2Ag2a_order.go:154G2A
G2A price-mismatchunit_price/auction_base_price stored but ignored; billing uses mapping denominationG2Ag2a order pathG2A
TRS CancelVoucher is a no-op that returns success without calling the API; no tests, no mockVendortrs adapterTRS
No zero-downtime / no DB rollback / no version stamping — stop→migrate→start every deploy; binary rollback leaves schema forwardDeploydeploy/update-octopus.shDeploy & Release
migrate fresh drops the DB, unguarded on prod (guarded only on sandbox)Deploymigration.go:265Deploy & Release
Admin destructive actions confirm client-side only (~13 of ~30 views) — server enforces only write/super gatingAdminviews/admin/Admin Panel
Client-config payout limits computed & shown but not enforced on createPayoutspayout_service.goPayouts
No client-side login lockout (admin has a 5-strike; client has none)Authclient_auth.go:18Auth findings
Client portal sends no CSRF token — cookie auth + withCredentials with zero XSRF handling; rests entirely on backend SameSite=Lax — confirm the backend enforces origin/CSRF for state-changing /client/* callsClient Portalservices/api/client.tsClient Portal Auth
Client-portal webhook signing secret may be readable on list — the card renders a reveal/copy control for token on every listed webhook, contradicting the "only returned on create" contract; verify the Go GET /webhooks handlerClient Portalsettings/webhooks/WebhookCard.tsxClient Portal Settings
Grasshopper topup/eSIM reservation isn't concurrency-safe — the reserve UPDATE gates on is_redeemed=false but never sets it, so concurrent submits fire multiple Octopus order calls (only plain gift-card is single-winner); pinned by a TODO(BUG) witness test asserting octopusCalls > 1Grasshoppersrc/api/claim.ts:849Test Catalog
Large CI blind spotsservices (incl. payout engine), email, middleware, jobs (shopify jobs, payout job), http/handler (incl. admin_ui.go, create_voucher_order.go), test/{admin,clientportal,flows,frontend}, and all Grasshopper vitest are real suites that no workflow runsTesting.github/workflows/test-backend-api.ymlTest Catalog
mocky-balboa /_chaos is unauthenticated and chaos rules are global rows in a shared D1 — against the deployed mock, concurrent CI runs' DisableAll/arm interfere (documented flakiness); its custom domain is also absent from wrangler.jsonc (dashboard-only)Testing/Mockmocky routes/_chaos.ts, wrangler.jsoncMocky-Balboa
Grasshopper poll recovery creates orders inside a GET — idempotency rests entirely on Octopus honoring the reused client_reference; exposed to browser prefetch/retryGrasshopperclaim.ts:1192Grasshopper
Grasshopper rate limiters fail open on a missing binding; RATE_LIMIT_FAIL_OPEN=true ships in .dev.vars.example (copy-to-prod hazard)Grasshopperrate-limit.ts:39Grasshopper
Grasshopper vouchers has no indexescode/poll_token/topup_client_ref lookups are unindexed table scans; unsalted SHA-256 for 6-digit PINsGrasshopperschema.tsGrasshopper
currencies.is_active enforced nowhere — every currency lookup filters only deleted_at IS NULL, so "deactivating" a currency does not stop orders in it; the only real blocks are soft-delete, remove the product, or drop the FX rateMoneyrepo/currency.go:270, repo/forex.go:26Troubleshooting
Voucher retry cap (10) vs recreate budget (5) mismatch — orders at retry_count 6–10 are still selected by pending-order-retry but refuse to recreate; above 10 they silently drop out of the query with no alert, no FAILED, no refund (top-up cron has the same 10-vs-5 split)Orders/Top-uprepo/order.go:1557, bulk_vendor_order_checkpoints.go:695Troubleshooting

🟢 Low — dead code, stale comments, doc drift

IssueAreaWhere
direct_topup_webhooks table/repo appears dead — webhooks persist to vendor_webhook_logsVendor (DTONE)DT One
RechargeStatusRecharged dead; "PROCESSING" literal guard is a dead branchTop-upTop-up & eSIM
eSIM sub-statuses REFUNDED/NOT_REFUNDED/COMPLETED defined but never assignedeSIMTop-up & eSIM
wallet_refund.go dead trio, payout.refunded event, WISE/REVOLUT codes, POSTPAID type, GST unimplementedWallet/PayoutsWallet, Payouts
shopify_order_item_vouchers table orphaned; shopify_last_sync_at written but never readShopifyShopify
G2A webhook_secret, G2AReservation.G2AOrderID, unused status constantsG2AG2A
OrderSource SHOPIFY/DASHBOARD constants defined but never assignedOrderOrder Lifecycle
Referenced-but-absent: automation/scripts/run-go-tests.sh, make setup, database/repo/TESTING.mdTestingTesting
Stale cron-schedule comments (trust the cron string, not the comment); retry "Max 5" is really 10JobsJobs & Observability
recharges.sub_status column exists (migration 20260427000000) but the Recharge model has no field and the repo never selects it → write-only, invisible to the app read pathTop-upTroubleshooting
GetMissingForexRatesForWallets hardcodes wallet currencies to {INR, USD} — a client whose base currency is neither won't be reported as missing a rateMoneyTroubleshooting
client_vendor_blacklists checked inconsistently — the hide-list path filters is_active, the vendor-attach path filters only deleted_atCatalogTroubleshooting
Two competing docs deploy paths (Cloudflare Pages CI vs nginx-static on box)DeployDeploy & Release
latest image tags for valkey/otel-collector — non-reproducible pullsDeployDeploy & Release
Two default-discount policies diverge (−1.0 vs −4.0)CatalogProducts & Catalog
bcrypt cost mismatch (admin 12 vs client/API 10); dead CookieAccessExpiry=15m; duplicated checkIPWhitelistAuthAuth findings
Client portal deploy doc is stale — the app migrated to a standalone Node/systemd + nginx deploy (output:'standalone', deploy/octopus-client.service), but frontend/client/ARCHITECTURE.md + a leftover out/ dir still describe Cloudflare Pages static exportClient PortalClient Portal
Client portal: Shopify not implemented (feature flag + dead #! upsell only); IP-whitelist has no client-side CIDR validation and empty=allow-all with no-confirm disable; Account Info is read-only (no update endpoint)Client PortalClient Portal Settings
Grasshopper dead code: ErrorPage.tsx, createClaimSession/verifyClaimSession, isPending=true const, PRIVATE var, octopus_esim_product_id (fulfilment reads octopus_topup_product_id), PATCH /api/vouchers/code/:code/redeem (no Go caller — GH self-redeems)GrasshopperGrasshopper, API
Grasshopper esc() doesn't escape ' yet escaped values sit inside single-quoted inline onclick JS strings (ICCID copy buttons); voucherInfoBar/redeemedHtml skip esc() on some DB valuesGrasshopperUI
Dual-hash split — GH stores SHA-256(code) for D1 lookups but Octopus redemption matches SHA-3-256 (voucher_redemption.go:32); assuming they're the same hash is a footgunGrasshopper/VoucherAPI
GET /api/vouchers/code/:code returns the entire row (incl. pin_hash, poll_token, encrypted notification_email) to any admin tokenGrasshopperAPI
Grasshopper CI runs plain wrangler deploy (no --minify that the npm script uses); reset-remote.sh uses an order-dependent sed on the first database_idGrasshopperGrasshopper
Seeders have no prod guard — running a vendor/credential seeder on a shared env overwrites live creds with localhost mock values; committed dev creds in seeders (GH api_key, Runa whsec_, admin default password)SeedersSeed Catalog
Grasshopper L4 e2e absent (test/e2e/ missing so test:e2e runs nothing); fuzz iteration counts capped low to avoid isolate OOM; RateLimit branches only stub-testedTestingTest Catalog
Seeder ordering fragility — voucher orchestration suites self-seed because the shared vendor_product seeder only assigns variants to the first 10 vendorsSeedersSeed Catalog
Adapter-level test gaps — TRS/GH/iRewardify have no adapter tests; Wupex-topup/DTOne-eSIM have no contract test; SEAGM money-mapping fixed-but-untested (the ~60× overcharge path); FailureInfo declared 3× (DRY); Svix webhook verifier untestedVendor/TestingVendor Adapter Tests
No orchestration test suite for TRS / EpinForce / Grasshopper; TRS wholly untestedTestingTesting

Suggested first sprint

If picking up cold, the highest value-per-effort order:

  1. Rotate every committed secret (§🔴 committed secrets) — cheapest, highest risk. Then scrub or .gitignore-verify each source.
  2. Fix the Shopify client_id=0 bug — one-line struct fix, unblocks all multi-tenant ingest.
  3. Decide the payout pipeline — either register the crons + wire QueuePayout + add the refund branch, or feature-flag payouts off until it's finished. Today it silently debits wallets with no fulfilment.
  4. Harden the top-up webhook to eSIM-webhook parity (lock + terminal short-circuit + auto-refund).
  5. Wire CSRF back on and add rate limiting to the auth endpoints.
  6. Consolidate the two refund implementations and delete the dead wallet_refund.go (or wire it in).

This index is a snapshot from the documentation pass. As issues are fixed, delete the row (and update the linked page) — a shrinking triage list is the clearest signal of handover progress.

On this page